@planningcenter/chat-react-native 3.37.0 → 3.37.1-qa-747.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.
Files changed (122) hide show
  1. package/build/components/conversation/jump_to_bottom_button.d.ts +1 -2
  2. package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
  3. package/build/components/conversation/jump_to_bottom_button.js +7 -39
  4. package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
  5. package/build/components/conversation/reply_shadow_message.d.ts +2 -1
  6. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  7. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  8. package/build/contexts/conversation_context.d.ts +1 -8
  9. package/build/contexts/conversation_context.d.ts.map +1 -1
  10. package/build/contexts/conversation_context.js +3 -21
  11. package/build/contexts/conversation_context.js.map +1 -1
  12. package/build/hooks/use_conversation_messages.d.ts +6 -15
  13. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  14. package/build/hooks/use_conversation_messages.js +9 -62
  15. package/build/hooks/use_conversation_messages.js.map +1 -1
  16. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  17. package/build/hooks/use_conversation_messages_jolt_events.js +4 -4
  18. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  19. package/build/hooks/use_conversations_actions.d.ts +0 -5
  20. package/build/hooks/use_conversations_actions.d.ts.map +1 -1
  21. package/build/hooks/use_conversations_actions.js +0 -12
  22. package/build/hooks/use_conversations_actions.js.map +1 -1
  23. package/build/hooks/use_features.d.ts +0 -1
  24. package/build/hooks/use_features.d.ts.map +1 -1
  25. package/build/hooks/use_features.js +0 -1
  26. package/build/hooks/use_features.js.map +1 -1
  27. package/build/hooks/use_mark_latest_message_read.d.ts +1 -1
  28. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  29. package/build/hooks/use_mark_latest_message_read.js +1 -17
  30. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  31. package/build/hooks/use_suspense_api.d.ts +0 -1
  32. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  33. package/build/hooks/use_suspense_api.js +1 -1
  34. package/build/hooks/use_suspense_api.js.map +1 -1
  35. package/build/screens/conversation_screen.d.ts +0 -1
  36. package/build/screens/conversation_screen.d.ts.map +1 -1
  37. package/build/screens/conversation_screen.js +45 -96
  38. package/build/screens/conversation_screen.js.map +1 -1
  39. package/build/utils/cache/messages_cache.d.ts +0 -1
  40. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  41. package/build/utils/cache/messages_cache.js +0 -4
  42. package/build/utils/cache/messages_cache.js.map +1 -1
  43. package/build/utils/group_messages.d.ts +2 -9
  44. package/build/utils/group_messages.d.ts.map +1 -1
  45. package/build/utils/group_messages.js +1 -20
  46. package/build/utils/group_messages.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/components/conversation/jump_to_bottom_button.tsx +8 -57
  49. package/src/components/conversation/reply_shadow_message.tsx +1 -1
  50. package/src/contexts/conversation_context.tsx +2 -30
  51. package/src/hooks/use_conversation_messages.ts +20 -120
  52. package/src/hooks/use_conversation_messages_jolt_events.ts +3 -4
  53. package/src/hooks/use_conversations_actions.ts +0 -15
  54. package/src/hooks/use_features.ts +0 -1
  55. package/src/hooks/use_mark_latest_message_read.ts +2 -16
  56. package/src/hooks/use_suspense_api.ts +1 -1
  57. package/src/screens/conversation_screen.tsx +69 -186
  58. package/src/utils/__tests__/group_messages.test.ts +0 -71
  59. package/src/utils/cache/messages_cache.ts +0 -5
  60. package/src/utils/group_messages.ts +2 -42
  61. package/build/components/conversation/unread_divider.d.ts +0 -6
  62. package/build/components/conversation/unread_divider.d.ts.map +0 -1
  63. package/build/components/conversation/unread_divider.js +0 -59
  64. package/build/components/conversation/unread_divider.js.map +0 -1
  65. package/build/hooks/use_flat_list_viewability.d.ts +0 -20
  66. package/build/hooks/use_flat_list_viewability.d.ts.map +0 -1
  67. package/build/hooks/use_flat_list_viewability.js +0 -30
  68. package/build/hooks/use_flat_list_viewability.js.map +0 -1
  69. package/build/hooks/use_jump_to_bottom_action.d.ts +0 -9
  70. package/build/hooks/use_jump_to_bottom_action.d.ts.map +0 -1
  71. package/build/hooks/use_jump_to_bottom_action.js +0 -62
  72. package/build/hooks/use_jump_to_bottom_action.js.map +0 -1
  73. package/build/hooks/use_jump_to_unread_anchor.d.ts +0 -20
  74. package/build/hooks/use_jump_to_unread_anchor.d.ts.map +0 -1
  75. package/build/hooks/use_jump_to_unread_anchor.js +0 -53
  76. package/build/hooks/use_jump_to_unread_anchor.js.map +0 -1
  77. package/build/hooks/use_jump_to_unread_gates.d.ts +0 -5
  78. package/build/hooks/use_jump_to_unread_gates.d.ts.map +0 -1
  79. package/build/hooks/use_jump_to_unread_gates.js +0 -10
  80. package/build/hooks/use_jump_to_unread_gates.js.map +0 -1
  81. package/build/hooks/use_scroll_tracking.d.ts +0 -13
  82. package/build/hooks/use_scroll_tracking.d.ts.map +0 -1
  83. package/build/hooks/use_scroll_tracking.js +0 -45
  84. package/build/hooks/use_scroll_tracking.js.map +0 -1
  85. package/build/hooks/use_track_highest_seen_message.d.ts +0 -4
  86. package/build/hooks/use_track_highest_seen_message.d.ts.map +0 -1
  87. package/build/hooks/use_track_highest_seen_message.js +0 -35
  88. package/build/hooks/use_track_highest_seen_message.js.map +0 -1
  89. package/build/utils/conversation_messages.d.ts +0 -10
  90. package/build/utils/conversation_messages.d.ts.map +0 -1
  91. package/build/utils/conversation_messages.js +0 -22
  92. package/build/utils/conversation_messages.js.map +0 -1
  93. package/build/utils/highest_seen_tracker.d.ts +0 -12
  94. package/build/utils/highest_seen_tracker.d.ts.map +0 -1
  95. package/build/utils/highest_seen_tracker.js +0 -37
  96. package/build/utils/highest_seen_tracker.js.map +0 -1
  97. package/build/utils/message_viewability.d.ts +0 -24
  98. package/build/utils/message_viewability.d.ts.map +0 -1
  99. package/build/utils/message_viewability.js +0 -29
  100. package/build/utils/message_viewability.js.map +0 -1
  101. package/build/utils/unread_divider_helpers.d.ts +0 -18
  102. package/build/utils/unread_divider_helpers.d.ts.map +0 -1
  103. package/build/utils/unread_divider_helpers.js +0 -13
  104. package/build/utils/unread_divider_helpers.js.map +0 -1
  105. package/src/__tests__/hooks/use_conversation_messages.test.tsx +0 -109
  106. package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +0 -154
  107. package/src/__tests__/utils/cache/messages_cache.test.ts +0 -54
  108. package/src/components/conversation/unread_divider.tsx +0 -90
  109. package/src/hooks/use_flat_list_viewability.ts +0 -50
  110. package/src/hooks/use_jump_to_bottom_action.ts +0 -75
  111. package/src/hooks/use_jump_to_unread_anchor.ts +0 -68
  112. package/src/hooks/use_jump_to_unread_gates.ts +0 -10
  113. package/src/hooks/use_scroll_tracking.ts +0 -64
  114. package/src/hooks/use_track_highest_seen_message.ts +0 -43
  115. package/src/utils/__tests__/conversation_messages.test.ts +0 -105
  116. package/src/utils/__tests__/highest_seen_tracker.test.ts +0 -82
  117. package/src/utils/__tests__/message_viewability.test.ts +0 -168
  118. package/src/utils/__tests__/unread_divider_helpers.test.ts +0 -85
  119. package/src/utils/conversation_messages.ts +0 -37
  120. package/src/utils/highest_seen_tracker.ts +0 -42
  121. package/src/utils/message_viewability.ts +0 -49
  122. package/src/utils/unread_divider_helpers.ts +0 -25
@@ -1,19 +1,15 @@
1
1
  import { debounce } from 'lodash'
2
2
  import { useEffect, useMemo, useRef } from 'react'
3
- import { useConversationContext } from '../contexts/conversation_context'
4
3
  import { ConversationResource, MessageResource } from '../types'
5
4
  import { useAppState } from './use_app_state'
6
5
  import { useConversationsMarkRead } from './use_conversations_actions'
7
- import { useJumpToUnreadGates } from './use_jump_to_unread_gates'
8
6
 
9
7
  interface Props {
10
8
  conversation: ConversationResource
11
- messages?: MessageResource[]
9
+ messages: MessageResource[]
12
10
  }
13
11
 
14
12
  export function useMarkLatestMessageRead({ conversation }: Props) {
15
- const { jumpToUnreadActive } = useJumpToUnreadGates()
16
- const { currentPageReplyRootId, atEndOfMessageHistory } = useConversationContext()
17
13
  const firedOnce = useRef<boolean>(false)
18
14
  const { markRead } = useConversationsMarkRead({ conversation })
19
15
  const debouncedMarkRead = useMemo(
@@ -29,20 +25,10 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
29
25
 
30
26
  useEffect(() => {
31
27
  if (!isActive || !shouldMarkRead) return
32
- if (currentPageReplyRootId) return
33
- if (jumpToUnreadActive && !atEndOfMessageHistory) return
34
28
 
35
29
  firedOnce.current = true
36
30
 
37
31
  debouncedMarkRead(true)
38
32
  // keeping unreadReactionCount in the dependency array to watch for changes
39
- }, [
40
- debouncedMarkRead,
41
- isActive,
42
- shouldMarkRead,
43
- unreadReactionCount,
44
- currentPageReplyRootId,
45
- jumpToUnreadActive,
46
- atEndOfMessageHistory,
47
- ])
33
+ }, [debouncedMarkRead, isActive, shouldMarkRead, unreadReactionCount])
48
34
  }
@@ -90,7 +90,7 @@ export const useSuspensePaginator = <T extends ResourceObject>(
90
90
  return { ...query, data, totalCount }
91
91
  }
92
92
 
93
- export const throwResponseError = (error: unknown) => {
93
+ const throwResponseError = (error: unknown) => {
94
94
  if (error instanceof Response) {
95
95
  throw new ResponseError(error as FailedResponse)
96
96
  }
@@ -8,9 +8,8 @@ import {
8
8
  useTheme as useNavigationTheme,
9
9
  useRoute,
10
10
  } from '@react-navigation/native'
11
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
12
- import { ActivityIndicator, FlatList, Platform, StyleSheet, View } from 'react-native'
13
- import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
11
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
12
+ import { FlatList, Platform, StyleSheet, View } from 'react-native'
14
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
15
14
  import { Badge, Icon, Text } from '../components'
16
15
  import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
@@ -25,46 +24,25 @@ import {
25
24
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
26
25
  import { SystemMessage } from '../components/conversation/system_message'
27
26
  import { TypingIndicator } from '../components/conversation/typing_indicator'
28
- import { UnreadDivider } from '../components/conversation/unread_divider'
29
27
  import { KeyboardView } from '../components/display/keyboard_view'
30
28
  import BlankState from '../components/primitive/blank_state_primitive'
31
- import {
32
- ConversationContextProvider,
33
- useConversationContext,
34
- } from '../contexts/conversation_context'
29
+ import { ConversationContextProvider } from '../contexts/conversation_context'
35
30
  import { useTheme } from '../hooks'
36
31
  import { useConversation } from '../hooks/use_conversation'
37
32
  import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
38
33
  import { useConversationMessages } from '../hooks/use_conversation_messages'
39
34
  import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
40
- import { availableFeatures, useFeatures } from '../hooks/use_features'
41
- import { useFlatListViewability } from '../hooks/use_flat_list_viewability'
42
- import { useJumpToBottomAction } from '../hooks/use_jump_to_bottom_action'
43
- import { useJumpToUnreadAnchor } from '../hooks/use_jump_to_unread_anchor'
44
- import { useJumpToUnreadGates } from '../hooks/use_jump_to_unread_gates'
45
35
  import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
46
36
  import {
47
37
  analyticsEvents,
48
38
  normalizeAnalyticsMetadata,
49
39
  usePublishProductAnalyticsEvent,
50
40
  } from '../hooks/use_product_analytics'
51
- import { useScrollTracking } from '../hooks/use_scroll_tracking'
52
- import { useTrackHighestSeenMessage } from '../hooks/use_track_highest_seen_message'
53
41
  import { ConversationResource } from '../types/resources/conversation'
54
42
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
55
43
  import { MessageResource } from '../types/resources/message'
56
44
  import { getRelativeDateStatus } from '../utils/date'
57
- import {
58
- groupMessages,
59
- UNREAD_DIVIDER_KEY,
60
- type DateSeparator,
61
- type EnrichedMessage,
62
- } from '../utils/group_messages'
63
- import {
64
- detectDividerExitTowardNewer,
65
- reportViewableMessages,
66
- type ViewabilityObserver,
67
- } from '../utils/message_viewability'
45
+ import { groupMessages, type DateSeparator } from '../utils/group_messages'
68
46
  import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
69
47
  import { isSystemMessage } from '../utils/system_messages'
70
48
 
@@ -75,7 +53,6 @@ export type ConversationRouteProps = {
75
53
  chat_group_graph_id?: string
76
54
  clear_input?: boolean
77
55
  editing_message_id?: number | null
78
- message_id?: string
79
56
  title?: string
80
57
  subtitle?: string
81
58
  badge?: ConversationBadgeResource
@@ -85,34 +62,20 @@ export type ConversationRouteProps = {
85
62
 
86
63
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
87
64
 
88
- const extractItemKey = (item: EnrichedMessage) => String(item.id)
89
- const maintainVisibleContentPosition = { minIndexForVisible: 0 }
90
-
91
65
  export function ConversationScreen({ route }: ConversationScreenProps) {
92
- const { conversation_id, message_id, reply_root_id } = route.params
66
+ const { conversation_id, reply_root_id } = route.params
93
67
 
94
68
  const { data: conversation } = useConversation({ conversation_id })
95
- const { featureEnabled } = useFeatures()
96
69
 
97
70
  usePublishProductAnalyticsEvent(analyticsEvents.conversation_show_opened, {
98
71
  reply_root_id,
99
72
  ...normalizeAnalyticsMetadata(conversation),
100
73
  })
101
74
 
102
- const lastReadMessageSortKey = conversation.conversationMembership?.lastReadMessageSortKey ?? null
103
- const jumpToUnreadAnchor =
104
- featureEnabled(availableFeatures.jump_to_unread) && !reply_root_id
105
- ? lastReadMessageSortKey
106
- : null
107
- const initialMessageId = message_id ?? jumpToUnreadAnchor
108
- const initialMessageIdIsAnchor = !!initialMessageId && !message_id
109
-
110
75
  return (
111
76
  <ConversationContextProvider
112
77
  conversationId={conversation_id}
113
78
  currentPageReplyRootId={reply_root_id ?? null}
114
- initialMessageId={initialMessageId}
115
- initialMessageIdIsAnchor={initialMessageIdIsAnchor}
116
79
  >
117
80
  <ConversationScreenContent route={route} />
118
81
  </ConversationContextProvider>
@@ -122,49 +85,29 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
122
85
  function ConversationScreenContent({ route }: ConversationScreenProps) {
123
86
  const styles = useStyles()
124
87
  const navigation = useNavigation()
125
- const {
126
- conversation_id: conversationId,
127
- editing_message_id: editingMessageId,
128
- reply_root_id: replyRootId,
129
- reply_root_author_name: replyRootAuthorName,
130
- } = route.params
88
+ const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
89
+ route.params
131
90
  const { data: conversation } = useConversation(route.params)
132
- const {
133
- messages,
134
- fetchOlderMessages,
135
- fetchNewerMessages,
136
- hasMoreNewerMessages,
137
- isFetchingNewerMessages,
138
- cancelFetchNewerMessages,
139
- } = useConversationMessages({ conversation_id: conversationId, reply_root_id: replyRootId })
140
-
141
- const { jumpToUnreadActive } = useJumpToUnreadGates()
142
- const { initialMessageId } = useConversationContext()
143
-
144
- useConversationJoltEvents({ conversationId })
145
- useConversationMessagesJoltEvents({ conversationId })
91
+ const { messages, fetchNextPage } = useConversationMessages({
92
+ conversation_id,
93
+ reply_root_id,
94
+ })
95
+ useConversationJoltEvents({ conversationId: conversation_id })
96
+ useConversationMessagesJoltEvents({ conversationId: conversation_id })
146
97
  useEnsureConversationsRouteExists()
147
98
  useMarkLatestMessageRead({ conversation, messages })
148
- const { onMessageSeen } = useTrackHighestSeenMessage()
149
-
150
- const items = useMemo(
151
- () =>
152
- groupMessages({
153
- ms: messages,
154
- inReplyScreen: !!replyRootId,
155
- jumpToUnreadActive,
156
- initialMessageId,
157
- }),
158
- [messages, replyRootId, jumpToUnreadActive, initialMessageId]
159
- )
160
- const noMessages = items.length === 0
99
+ const messagesWithSeparators = groupMessages({
100
+ ms: messages,
101
+ inReplyScreen: !!reply_root_id,
102
+ })
103
+ const noMessages = messagesWithSeparators.length === 0
161
104
 
162
105
  const { repliesDisabled, memberAbility, badges, title } = conversation
163
106
  const canReply = memberAbility?.canReply
164
107
  const showLeaderDisabledReplyBanner = canReply && repliesDisabled
165
108
  const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
166
- const currentlyEditingMessage = messages.find(m => String(m.id) === String(editingMessageId))
167
- const replyRootAuthorFirstName = replyRootAuthorName?.split(' ')[0]
109
+ const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))
110
+ const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]
168
111
  const replyHeaderTitle = replyRootAuthorFirstName
169
112
  ? `Reply to ${replyRootAuthorFirstName}`
170
113
  : 'Reply'
@@ -172,96 +115,21 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
172
115
  const muted = conversation.conversationMembership?.muted ?? conversation.muted
173
116
 
174
117
  const listRef = useRef<FlatList>(null)
175
- const [dividerScrolledPast, setDividerScrolledPast] = useState(false)
176
-
177
- const observers = useMemo<ViewabilityObserver<EnrichedMessage>[]>(
178
- () => [
179
- reportViewableMessages(onMessageSeen),
180
- detectDividerExitTowardNewer({
181
- dividerKey: UNREAD_DIVIDER_KEY,
182
- initialMessageId,
183
- onExited: () => setDividerScrolledPast(true),
184
- }),
185
- ],
186
- [onMessageSeen, initialMessageId]
187
- )
118
+ const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
188
119
 
189
- const { viewabilityConfigCallbackPairs, onScrollBeginDrag: viewabilityOnScrollBeginDrag } =
190
- useFlatListViewability({ observers })
191
- const {
192
- onContentSizeChange,
193
- onScrollToIndexFailed,
194
- onScrollBeginDrag: anchorOnScrollBeginDrag,
195
- } = useJumpToUnreadAnchor({ listRef, items })
196
- const onScrollBeginDrag = useCallback(() => {
197
- viewabilityOnScrollBeginDrag()
198
- anchorOnScrollBeginDrag()
199
- }, [viewabilityOnScrollBeginDrag, anchorOnScrollBeginDrag])
200
- const { onScroll, showJumpToBottomButton } = useScrollTracking({
201
- hasMoreNewerMessages,
202
- isFetchingNewerMessages,
203
- fetchNewerMessages,
204
- cancelFetchNewerMessages,
205
- })
206
- const { handleJumpToBottom, isJumpingToBottom } = useJumpToBottomAction({ listRef })
207
-
208
- const listHeader = useMemo(
209
- () => (
210
- <View>
211
- {isFetchingNewerMessages && (
212
- <Animated.View
213
- entering={FadeIn.duration(750)}
214
- exiting={FadeOut.duration(750)}
215
- style={styles.loadingFooter}
216
- accessibilityRole="progressbar"
217
- accessibilityLabel="Loading more messages"
218
- >
219
- <ActivityIndicator />
220
- </Animated.View>
221
- )}
222
- <View style={styles.listHeader} />
223
- </View>
224
- ),
225
- [isFetchingNewerMessages, styles.loadingFooter, styles.listHeader]
226
- )
120
+ const trackScroll = (event: any) => {
121
+ const offsetY = event.nativeEvent.contentOffset.y
122
+ setShowJumpToBottomButton(offsetY > 200)
123
+ }
227
124
 
228
- const renderItem = useCallback(
229
- ({ item }: { item: EnrichedMessage }) => {
230
- if (item.type === 'DateSeparator') return <InlineDateSeparator {...item} />
231
- if (item.type === 'UnreadDivider') return <UnreadDivider scrolledPast={dividerScrolledPast} />
232
- if (item.type === 'ReplyShadowMessage') {
233
- return (
234
- <ReplyShadowMessage
235
- {...item}
236
- conversation_id={conversationId}
237
- inReplyScreen={!!replyRootId}
238
- />
239
- )
240
- }
241
- if (isSystemMessage(item)) {
242
- return <SystemMessage message={item} conversationId={conversationId} />
243
- }
244
- return (
245
- <Message
246
- {...item}
247
- canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
248
- conversation_id={conversationId}
249
- latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
250
- inReplyScreen={!!replyRootId}
251
- />
252
- )
253
- },
254
- [
255
- dividerScrolledPast,
256
- conversationId,
257
- replyRootId,
258
- canDeleteNonAuthoredMessages,
259
- conversation?.latestReadMessageSortKey,
260
- ]
261
- )
125
+ const handleReturnToBottom = useCallback(() => {
126
+ listRef.current?.scrollToOffset({
127
+ offset: 0,
128
+ })
129
+ }, [])
262
130
 
263
131
  useEffect(() => {
264
- if (replyRootId) {
132
+ if (reply_root_id) {
265
133
  navigation.setParams({
266
134
  title: replyHeaderTitle,
267
135
  })
@@ -273,7 +141,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
273
141
  muted,
274
142
  })
275
143
  }
276
- }, [navigation, title, badges, conversation?.deleted, replyRootId, replyHeaderTitle, muted])
144
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle, muted])
277
145
 
278
146
  if (!conversation || conversation.deleted) {
279
147
  return (
@@ -304,32 +172,51 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
304
172
  inverted
305
173
  ref={listRef}
306
174
  contentContainerStyle={styles.listContainer}
307
- maintainVisibleContentPosition={maintainVisibleContentPosition}
308
- data={items}
309
- keyExtractor={extractItemKey}
310
- onScroll={onScroll}
311
- onScrollBeginDrag={onScrollBeginDrag}
312
- scrollEventThrottle={64}
313
- viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
314
- onContentSizeChange={onContentSizeChange}
315
- onScrollToIndexFailed={onScrollToIndexFailed}
316
- renderItem={renderItem}
317
- onEndReached={() => fetchOlderMessages()}
318
- ListHeaderComponent={listHeader}
175
+ data={messagesWithSeparators}
176
+ keyExtractor={item => item.id}
177
+ onScroll={trackScroll}
178
+ scrollEventThrottle={10}
179
+ renderItem={({ item }) => {
180
+ if (item.type === 'DateSeparator') {
181
+ return <InlineDateSeparator {...item} />
182
+ }
183
+
184
+ if (item.type === 'ReplyShadowMessage') {
185
+ return (
186
+ <ReplyShadowMessage
187
+ {...(item as any)}
188
+ conversation_id={conversation_id}
189
+ inReplyScreen={!!reply_root_id}
190
+ />
191
+ )
192
+ }
193
+
194
+ if (isSystemMessage(item)) {
195
+ return <SystemMessage message={item} conversationId={conversation_id} />
196
+ }
197
+
198
+ return (
199
+ <Message
200
+ {...item}
201
+ canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
202
+ conversation_id={conversation_id}
203
+ latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
204
+ inReplyScreen={!!reply_root_id}
205
+ />
206
+ )
207
+ }}
208
+ onEndReached={() => fetchNextPage()}
209
+ ListHeaderComponent={<View style={styles.listHeader} />}
319
210
  />
320
211
  )}
321
- <JumpToBottomButton
322
- onPress={handleJumpToBottom}
323
- visible={showJumpToBottomButton}
324
- loading={isJumpingToBottom}
325
- />
212
+ <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
326
213
  {!noMessages && <TypingIndicator />}
327
214
  {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
328
215
  <ConversationBottomBar
329
216
  conversation={conversation}
330
217
  canReply={canReply}
331
218
  replyRootAuthorFirstName={replyRootAuthorFirstName}
332
- replyRootId={replyRootId}
219
+ replyRootId={reply_root_id}
333
220
  currentlyEditingMessage={currentlyEditingMessage}
334
221
  />
335
222
  </KeyboardView>
@@ -506,10 +393,6 @@ const useStyles = () => {
506
393
  // Just whitespace to provide space where the typing indicator can be
507
394
  height: 16,
508
395
  },
509
- loadingFooter: {
510
- paddingVertical: 12,
511
- alignItems: 'center',
512
- },
513
396
  })
514
397
  }
515
398
 
@@ -124,77 +124,6 @@ describe('groupMessages — nextRendersAuthor mirrors the newer enriched neighbo
124
124
  })
125
125
  })
126
126
 
127
- describe('groupMessages — unread divider', () => {
128
- it('inserts the divider between the read and unread boundary when jumpToUnreadActive', () => {
129
- const messages = [
130
- message('04', { createdAt: '2026-01-01T00:04:00Z' }),
131
- message('03', { createdAt: '2026-01-01T00:03:00Z' }),
132
- message('02', { createdAt: '2026-01-01T00:02:00Z' }),
133
- message('01', { createdAt: '2026-01-01T00:01:00Z' }),
134
- ]
135
-
136
- const enriched = groupMessages({
137
- ms: messages,
138
- jumpToUnreadActive: true,
139
- initialMessageId: '02',
140
- })
141
-
142
- const dividerIdx = enriched.findIndex(item => 'type' in item && item.type === 'UnreadDivider')
143
- const msg03Idx = enriched.findIndex(
144
- item => 'id' in item && item.id === '03' && !('type' in item && item.type !== 'Message')
145
- )
146
- const msg02Idx = enriched.findIndex(
147
- item => 'id' in item && item.id === '02' && !('type' in item && item.type !== 'Message')
148
- )
149
-
150
- expect(dividerIdx).toBeGreaterThan(msg03Idx)
151
- expect(dividerIdx).toBeLessThan(msg02Idx)
152
- })
153
-
154
- it('does not insert the divider when jumpToUnreadActive is false', () => {
155
- const enriched = groupMessages({
156
- ms: [message('02'), message('01')],
157
- jumpToUnreadActive: false,
158
- initialMessageId: '01',
159
- })
160
-
161
- expect(enriched.some(item => 'type' in item && item.type === 'UnreadDivider')).toBe(false)
162
- })
163
-
164
- it('does not insert the divider when no message crosses the boundary', () => {
165
- const enriched = groupMessages({
166
- ms: [message('05'), message('04'), message('03')],
167
- jumpToUnreadActive: true,
168
- initialMessageId: '02',
169
- })
170
-
171
- expect(enriched.some(item => 'type' in item && item.type === 'UnreadDivider')).toBe(false)
172
- })
173
-
174
- it('orders ULID-style ids consistently with backend sort_key comparisons', () => {
175
- const enriched = groupMessages({
176
- ms: [
177
- message('01KQSTAY189PHCJBT8T13R9VMP'),
178
- message('01KQST9HZAB10K3CXR7TYN2QWE'),
179
- message('01KQST73KZRPXNRDA7TYN19KXQ'),
180
- message('01KQST5JKZRPXNRDA7TYN19ABC'),
181
- ],
182
- jumpToUnreadActive: true,
183
- initialMessageId: '01KQST73KZRPXNRDA7TYN19KXQ',
184
- })
185
-
186
- const dividerIdx = enriched.findIndex(item => 'type' in item && item.type === 'UnreadDivider')
187
- const newerIdx = enriched.findIndex(
188
- item =>
189
- 'id' in item &&
190
- item.id === '01KQST9HZAB10K3CXR7TYN2QWE' &&
191
- !('type' in item && item.type !== 'Message')
192
- )
193
-
194
- expect(dividerIdx).toBeGreaterThan(newerIdx)
195
- })
196
- })
197
-
198
127
  describe('groupMessages — system messages', () => {
199
128
  it('flags lastInGroup true and renderAuthor false on system messages', () => {
200
129
  const messages = [
@@ -131,11 +131,6 @@ export function getThreadedMessagesQueryKey(conversationId: number, replyRootId:
131
131
  return getRequestQueryKey(requestArgs)
132
132
  }
133
133
 
134
- export function hasUnloadedNewerPages(queryClient: QueryClient, queryKey: unknown[]): boolean {
135
- const data = queryClient.getQueryData<MessagesQueryData>(queryKey)
136
- return !!data?.pages?.[0]?.meta?.next?.idGt
137
- }
138
-
139
134
  export function mergeMessageUpdate(
140
135
  record: MessageResource,
141
136
  current?: MessageResource
@@ -4,12 +4,8 @@ import { isSystemMessage } from './system_messages'
4
4
 
5
5
  const FIVE_MINUTES_MS = 5 * 60 * 1000
6
6
 
7
- export const UNREAD_DIVIDER_KEY = 'unread-divider'
8
-
9
7
  export type DateSeparator = { type: 'DateSeparator'; id: string; date: string }
10
8
 
11
- export type UnreadDividerItem = { type: 'UnreadDivider'; id: typeof UNREAD_DIVIDER_KEY }
12
-
13
9
  export type ReplyShadowMessage = {
14
10
  type: 'ReplyShadowMessage'
15
11
  id: string
@@ -18,25 +14,14 @@ export type ReplyShadowMessage = {
18
14
  nextRendersAuthor: boolean
19
15
  }
20
16
 
21
- export type EnrichedMessage =
22
- | MessageResource
23
- | DateSeparator
24
- | UnreadDividerItem
25
- | ReplyShadowMessage
17
+ export type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage
26
18
 
27
19
  interface GroupMessagesProps {
28
20
  ms: MessageResource[]
29
21
  inReplyScreen?: boolean
30
- jumpToUnreadActive?: boolean
31
- initialMessageId?: string | null
32
22
  }
33
23
 
34
- export function groupMessages({
35
- ms,
36
- inReplyScreen,
37
- jumpToUnreadActive,
38
- initialMessageId,
39
- }: GroupMessagesProps): EnrichedMessage[] {
24
+ export function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[] {
40
25
  const items: EnrichedMessage[] = []
41
26
  let myLatestSeen = false
42
27
  let nextNeighborEnriched: MessageResource | undefined
@@ -48,9 +33,6 @@ export function groupMessages({
48
33
  if (isSystemMessage(message)) {
49
34
  const enriched = enrichSystemMessage(message, next)
50
35
  items.push(enriched)
51
- if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {
52
- items.push(unreadDivider())
53
- }
54
36
  if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))
55
37
  nextNeighborEnriched = enriched
56
38
  return
@@ -62,10 +44,6 @@ export function groupMessages({
62
44
  const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen)
63
45
  items.push(enriched)
64
46
 
65
- if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {
66
- items.push(unreadDivider())
67
- }
68
-
69
47
  const shadow = replyShadowFor(enriched, prev)
70
48
  if (shadow) items.push(shadow)
71
49
 
@@ -79,24 +57,6 @@ export function groupMessages({
79
57
  return items
80
58
  }
81
59
 
82
- function crossesUnreadBoundary(
83
- message: MessageResource,
84
- prev: MessageResource | undefined,
85
- jumpToUnreadActive: boolean | undefined,
86
- initialMessageId: string | null | undefined
87
- ): boolean {
88
- if (!jumpToUnreadActive) return false
89
- if (!initialMessageId) return false
90
- if (!prev) return false
91
- return (
92
- prev.id.localeCompare(initialMessageId) <= 0 && message.id.localeCompare(initialMessageId) > 0
93
- )
94
- }
95
-
96
- function unreadDivider(): UnreadDividerItem {
97
- return { type: 'UnreadDivider', id: UNREAD_DIVIDER_KEY }
98
- }
99
-
100
60
  function neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {
101
61
  return { prev: arr[i + 1], next: arr[i - 1] }
102
62
  }
@@ -1,6 +0,0 @@
1
- interface UnreadDividerProps {
2
- scrolledPast?: boolean;
3
- }
4
- export declare function UnreadDivider({ scrolledPast }: UnreadDividerProps): import("react").JSX.Element | null;
5
- export {};
6
- //# sourceMappingURL=unread_divider.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"unread_divider.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/unread_divider.tsx"],"names":[],"mappings":"AAQA,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAMD,wBAAgB,aAAa,CAAC,EAAE,YAAoB,EAAE,EAAE,kBAAkB,sCAqBzE"}
@@ -1,59 +0,0 @@
1
- import { StyleSheet, View } from 'react-native';
2
- import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
3
- import Svg, { Defs, Path, Pattern, Rect } from 'react-native-svg';
4
- import { useConversationContext } from '../../contexts/conversation_context';
5
- import { useTheme } from '../../hooks';
6
- import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../../utils/styles';
7
- import { Text } from '../display';
8
- const WAVE_WIDTH = 16;
9
- const WAVE_HEIGHT = 8;
10
- const FADE_DURATION = 750;
11
- export function UnreadDivider({ scrolledPast = false }) {
12
- const styles = useStyles();
13
- const { atEndOfMessageHistory } = useConversationContext();
14
- if (scrolledPast || atEndOfMessageHistory)
15
- return null;
16
- return (<Animated.View entering={FadeIn.duration(FADE_DURATION)} exiting={FadeOut.duration(FADE_DURATION)} style={styles.container} accessibilityRole="header" accessibilityLabel="Unread messages start here">
17
- <SquigglyLine />
18
- <Text variant="footnote" style={styles.label}>
19
- New
20
- </Text>
21
- <SquigglyLine />
22
- </Animated.View>);
23
- }
24
- function SquigglyLine() {
25
- const { colors } = useTheme();
26
- return (<View style={squigglyStyle.container}>
27
- <Svg width="100%" height={WAVE_HEIGHT}>
28
- <Defs>
29
- <Pattern id="wave" x="0" y="0" width={WAVE_WIDTH} height={WAVE_HEIGHT} patternUnits="userSpaceOnUse">
30
- <Path d="M 0 4 Q 4 0 8 4 T 16 4" stroke={colors.interaction} strokeWidth={1.5} fill="none"/>
31
- </Pattern>
32
- </Defs>
33
- <Rect x="0" y="0" width="100%" height={WAVE_HEIGHT} fill="url(#wave)"/>
34
- </Svg>
35
- </View>);
36
- }
37
- const squigglyStyle = StyleSheet.create({
38
- container: {
39
- flex: 1,
40
- height: WAVE_HEIGHT,
41
- },
42
- });
43
- const useStyles = () => {
44
- const { colors } = useTheme();
45
- return StyleSheet.create({
46
- container: {
47
- alignItems: 'center',
48
- flexDirection: 'row',
49
- paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
50
- paddingVertical: 8,
51
- gap: 8,
52
- },
53
- label: {
54
- color: colors.interaction,
55
- fontWeight: '600',
56
- },
57
- });
58
- };
59
- //# sourceMappingURL=unread_divider.js.map