@planningcenter/chat-react-native 3.37.1-qa-736.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 (171) 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/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +7 -19
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversation/reply_shadow_message.d.ts +2 -1
  9. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  10. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  11. package/build/components/conversations/conversations.d.ts.map +1 -1
  12. package/build/components/conversations/conversations.js +16 -6
  13. package/build/components/conversations/conversations.js.map +1 -1
  14. package/build/contexts/conversation_context.d.ts +1 -8
  15. package/build/contexts/conversation_context.d.ts.map +1 -1
  16. package/build/contexts/conversation_context.js +3 -21
  17. package/build/contexts/conversation_context.js.map +1 -1
  18. package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
  19. package/build/hooks/use_attachment_uploader.js +0 -9
  20. package/build/hooks/use_attachment_uploader.js.map +1 -1
  21. package/build/hooks/use_conversation_messages.d.ts +6 -15
  22. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  23. package/build/hooks/use_conversation_messages.js +9 -62
  24. package/build/hooks/use_conversation_messages.js.map +1 -1
  25. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  26. package/build/hooks/use_conversation_messages_jolt_events.js +4 -4
  27. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  28. package/build/hooks/use_conversations_actions.d.ts +0 -5
  29. package/build/hooks/use_conversations_actions.d.ts.map +1 -1
  30. package/build/hooks/use_conversations_actions.js +0 -12
  31. package/build/hooks/use_conversations_actions.js.map +1 -1
  32. package/build/hooks/use_features.d.ts +0 -2
  33. package/build/hooks/use_features.d.ts.map +1 -1
  34. package/build/hooks/use_features.js +0 -2
  35. package/build/hooks/use_features.js.map +1 -1
  36. package/build/hooks/use_mark_latest_message_read.d.ts +1 -1
  37. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  38. package/build/hooks/use_mark_latest_message_read.js +1 -17
  39. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  40. package/build/hooks/use_suspense_api.d.ts +0 -1
  41. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  42. package/build/hooks/use_suspense_api.js +1 -1
  43. package/build/hooks/use_suspense_api.js.map +1 -1
  44. package/build/hooks/use_upload_client.d.ts +0 -4
  45. package/build/hooks/use_upload_client.d.ts.map +1 -1
  46. package/build/hooks/use_upload_client.js +1 -13
  47. package/build/hooks/use_upload_client.js.map +1 -1
  48. package/build/jest.js +1 -1
  49. package/build/jest.js.map +1 -1
  50. package/build/screens/age_check/age_check_underage_screen.js +1 -1
  51. package/build/screens/age_check/age_check_underage_screen.js.map +1 -1
  52. package/build/screens/avatar_picker/emoji_tab.d.ts.map +1 -1
  53. package/build/screens/avatar_picker/emoji_tab.js +6 -2
  54. package/build/screens/avatar_picker/emoji_tab.js.map +1 -1
  55. package/build/screens/conversation_screen.d.ts +0 -1
  56. package/build/screens/conversation_screen.d.ts.map +1 -1
  57. package/build/screens/conversation_screen.js +48 -95
  58. package/build/screens/conversation_screen.js.map +1 -1
  59. package/build/types/jolt_events/index.d.ts +1 -3
  60. package/build/types/jolt_events/index.d.ts.map +1 -1
  61. package/build/types/jolt_events/index.js.map +1 -1
  62. package/build/utils/cache/messages_cache.d.ts +0 -1
  63. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  64. package/build/utils/cache/messages_cache.js +0 -4
  65. package/build/utils/cache/messages_cache.js.map +1 -1
  66. package/build/utils/group_messages.d.ts +2 -9
  67. package/build/utils/group_messages.d.ts.map +1 -1
  68. package/build/utils/group_messages.js +1 -20
  69. package/build/utils/group_messages.js.map +1 -1
  70. package/package.json +4 -4
  71. package/src/__tests__/hooks/use_attachment_uploader.test.tsx +0 -36
  72. package/src/__tests__/jest.ts +1 -1
  73. package/src/components/conversation/jump_to_bottom_button.tsx +8 -57
  74. package/src/components/conversation/message_form.tsx +7 -21
  75. package/src/components/conversation/reply_shadow_message.tsx +1 -1
  76. package/src/components/conversations/conversations.tsx +16 -9
  77. package/src/contexts/conversation_context.tsx +2 -30
  78. package/src/hooks/use_attachment_uploader.ts +1 -14
  79. package/src/hooks/use_conversation_messages.ts +20 -120
  80. package/src/hooks/use_conversation_messages_jolt_events.ts +3 -4
  81. package/src/hooks/use_conversations_actions.ts +0 -15
  82. package/src/hooks/use_features.ts +0 -2
  83. package/src/hooks/use_mark_latest_message_read.ts +2 -16
  84. package/src/hooks/use_suspense_api.ts +1 -1
  85. package/src/hooks/use_upload_client.ts +1 -19
  86. package/src/jest.ts +1 -1
  87. package/src/screens/age_check/age_check_underage_screen.tsx +1 -1
  88. package/src/screens/avatar_picker/emoji_tab.tsx +6 -2
  89. package/src/screens/conversation_screen.tsx +76 -184
  90. package/src/types/jolt_events/index.ts +0 -3
  91. package/src/utils/__tests__/group_messages.test.ts +0 -71
  92. package/src/utils/cache/messages_cache.ts +0 -5
  93. package/src/utils/group_messages.ts +2 -42
  94. package/build/components/conversation/message_list.d.ts +0 -10
  95. package/build/components/conversation/message_list.d.ts.map +0 -1
  96. package/build/components/conversation/message_list.js +0 -13
  97. package/build/components/conversation/message_list.js.map +0 -1
  98. package/build/components/conversation/unread_divider.d.ts +0 -6
  99. package/build/components/conversation/unread_divider.d.ts.map +0 -1
  100. package/build/components/conversation/unread_divider.js +0 -59
  101. package/build/components/conversation/unread_divider.js.map +0 -1
  102. package/build/components/conversations/conversations_blank_state.d.ts +0 -8
  103. package/build/components/conversations/conversations_blank_state.d.ts.map +0 -1
  104. package/build/components/conversations/conversations_blank_state.js +0 -25
  105. package/build/components/conversations/conversations_blank_state.js.map +0 -1
  106. package/build/hooks/use_flat_list_viewability.d.ts +0 -20
  107. package/build/hooks/use_flat_list_viewability.d.ts.map +0 -1
  108. package/build/hooks/use_flat_list_viewability.js +0 -30
  109. package/build/hooks/use_flat_list_viewability.js.map +0 -1
  110. package/build/hooks/use_jump_to_bottom_action.d.ts +0 -9
  111. package/build/hooks/use_jump_to_bottom_action.d.ts.map +0 -1
  112. package/build/hooks/use_jump_to_bottom_action.js +0 -62
  113. package/build/hooks/use_jump_to_bottom_action.js.map +0 -1
  114. package/build/hooks/use_jump_to_unread_anchor.d.ts +0 -20
  115. package/build/hooks/use_jump_to_unread_anchor.d.ts.map +0 -1
  116. package/build/hooks/use_jump_to_unread_anchor.js +0 -53
  117. package/build/hooks/use_jump_to_unread_anchor.js.map +0 -1
  118. package/build/hooks/use_jump_to_unread_gates.d.ts +0 -5
  119. package/build/hooks/use_jump_to_unread_gates.d.ts.map +0 -1
  120. package/build/hooks/use_jump_to_unread_gates.js +0 -10
  121. package/build/hooks/use_jump_to_unread_gates.js.map +0 -1
  122. package/build/hooks/use_scroll_tracking.d.ts +0 -13
  123. package/build/hooks/use_scroll_tracking.d.ts.map +0 -1
  124. package/build/hooks/use_scroll_tracking.js +0 -45
  125. package/build/hooks/use_scroll_tracking.js.map +0 -1
  126. package/build/hooks/use_track_highest_seen_message.d.ts +0 -4
  127. package/build/hooks/use_track_highest_seen_message.d.ts.map +0 -1
  128. package/build/hooks/use_track_highest_seen_message.js +0 -35
  129. package/build/hooks/use_track_highest_seen_message.js.map +0 -1
  130. package/build/types/jolt_events/attachment_events.d.ts +0 -14
  131. package/build/types/jolt_events/attachment_events.d.ts.map +0 -1
  132. package/build/types/jolt_events/attachment_events.js +0 -2
  133. package/build/types/jolt_events/attachment_events.js.map +0 -1
  134. package/build/utils/conversation_messages.d.ts +0 -10
  135. package/build/utils/conversation_messages.d.ts.map +0 -1
  136. package/build/utils/conversation_messages.js +0 -22
  137. package/build/utils/conversation_messages.js.map +0 -1
  138. package/build/utils/highest_seen_tracker.d.ts +0 -12
  139. package/build/utils/highest_seen_tracker.d.ts.map +0 -1
  140. package/build/utils/highest_seen_tracker.js +0 -37
  141. package/build/utils/highest_seen_tracker.js.map +0 -1
  142. package/build/utils/message_viewability.d.ts +0 -24
  143. package/build/utils/message_viewability.d.ts.map +0 -1
  144. package/build/utils/message_viewability.js +0 -29
  145. package/build/utils/message_viewability.js.map +0 -1
  146. package/build/utils/unread_divider_helpers.d.ts +0 -18
  147. package/build/utils/unread_divider_helpers.d.ts.map +0 -1
  148. package/build/utils/unread_divider_helpers.js +0 -13
  149. package/build/utils/unread_divider_helpers.js.map +0 -1
  150. package/src/__tests__/hooks/use_conversation_messages.test.tsx +0 -109
  151. package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +0 -154
  152. package/src/__tests__/utils/cache/messages_cache.test.ts +0 -54
  153. package/src/components/conversation/__tests__/message_list.test.tsx +0 -14
  154. package/src/components/conversation/message_list.tsx +0 -42
  155. package/src/components/conversation/unread_divider.tsx +0 -90
  156. package/src/components/conversations/conversations_blank_state.tsx +0 -42
  157. package/src/hooks/use_flat_list_viewability.ts +0 -50
  158. package/src/hooks/use_jump_to_bottom_action.ts +0 -75
  159. package/src/hooks/use_jump_to_unread_anchor.ts +0 -68
  160. package/src/hooks/use_jump_to_unread_gates.ts +0 -10
  161. package/src/hooks/use_scroll_tracking.ts +0 -64
  162. package/src/hooks/use_track_highest_seen_message.ts +0 -43
  163. package/src/types/jolt_events/attachment_events.ts +0 -14
  164. package/src/utils/__tests__/conversation_messages.test.ts +0 -105
  165. package/src/utils/__tests__/highest_seen_tracker.test.ts +0 -82
  166. package/src/utils/__tests__/message_viewability.test.ts +0 -168
  167. package/src/utils/__tests__/unread_divider_helpers.test.ts +0 -85
  168. package/src/utils/conversation_messages.ts +0 -37
  169. package/src/utils/highest_seen_tracker.ts +0 -42
  170. package/src/utils/message_viewability.ts +0 -49
  171. package/src/utils/unread_divider_helpers.ts +0 -25
@@ -8,17 +8,14 @@ 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, Platform, StyleSheet, View } from 'react-native'
13
- import type { FlatList } from 'react-native-gesture-handler'
14
- 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'
15
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
16
14
  import { Badge, Icon, Text } from '../components'
17
15
  import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
18
16
  import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
19
17
  import { Message } from '../components/conversation/message'
20
18
  import { MessageForm } from '../components/conversation/message_form'
21
- import { MessageList } from '../components/conversation/message_list'
22
19
  import {
23
20
  ConversationDisabledBanner,
24
21
  LeaderMessagesDisabledBanner,
@@ -27,46 +24,25 @@ import {
27
24
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
28
25
  import { SystemMessage } from '../components/conversation/system_message'
29
26
  import { TypingIndicator } from '../components/conversation/typing_indicator'
30
- import { UnreadDivider } from '../components/conversation/unread_divider'
31
27
  import { KeyboardView } from '../components/display/keyboard_view'
32
28
  import BlankState from '../components/primitive/blank_state_primitive'
33
- import {
34
- ConversationContextProvider,
35
- useConversationContext,
36
- } from '../contexts/conversation_context'
29
+ import { ConversationContextProvider } from '../contexts/conversation_context'
37
30
  import { useTheme } from '../hooks'
38
31
  import { useConversation } from '../hooks/use_conversation'
39
32
  import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
40
33
  import { useConversationMessages } from '../hooks/use_conversation_messages'
41
34
  import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
42
- import { availableFeatures, useFeatures } from '../hooks/use_features'
43
- import { useFlatListViewability } from '../hooks/use_flat_list_viewability'
44
- import { useJumpToBottomAction } from '../hooks/use_jump_to_bottom_action'
45
- import { useJumpToUnreadAnchor } from '../hooks/use_jump_to_unread_anchor'
46
- import { useJumpToUnreadGates } from '../hooks/use_jump_to_unread_gates'
47
35
  import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
48
36
  import {
49
37
  analyticsEvents,
50
38
  normalizeAnalyticsMetadata,
51
39
  usePublishProductAnalyticsEvent,
52
40
  } from '../hooks/use_product_analytics'
53
- import { useScrollTracking } from '../hooks/use_scroll_tracking'
54
- import { useTrackHighestSeenMessage } from '../hooks/use_track_highest_seen_message'
55
41
  import { ConversationResource } from '../types/resources/conversation'
56
42
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
57
43
  import { MessageResource } from '../types/resources/message'
58
44
  import { getRelativeDateStatus } from '../utils/date'
59
- import {
60
- groupMessages,
61
- UNREAD_DIVIDER_KEY,
62
- type DateSeparator,
63
- type EnrichedMessage,
64
- } from '../utils/group_messages'
65
- import {
66
- detectDividerExitTowardNewer,
67
- reportViewableMessages,
68
- type ViewabilityObserver,
69
- } from '../utils/message_viewability'
45
+ import { groupMessages, type DateSeparator } from '../utils/group_messages'
70
46
  import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
71
47
  import { isSystemMessage } from '../utils/system_messages'
72
48
 
@@ -77,7 +53,6 @@ export type ConversationRouteProps = {
77
53
  chat_group_graph_id?: string
78
54
  clear_input?: boolean
79
55
  editing_message_id?: number | null
80
- message_id?: string
81
56
  title?: string
82
57
  subtitle?: string
83
58
  badge?: ConversationBadgeResource
@@ -88,30 +63,19 @@ export type ConversationRouteProps = {
88
63
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
89
64
 
90
65
  export function ConversationScreen({ route }: ConversationScreenProps) {
91
- const { conversation_id, message_id, reply_root_id } = route.params
66
+ const { conversation_id, reply_root_id } = route.params
92
67
 
93
68
  const { data: conversation } = useConversation({ conversation_id })
94
- const { featureEnabled } = useFeatures()
95
69
 
96
70
  usePublishProductAnalyticsEvent(analyticsEvents.conversation_show_opened, {
97
71
  reply_root_id,
98
72
  ...normalizeAnalyticsMetadata(conversation),
99
73
  })
100
74
 
101
- const lastReadMessageSortKey = conversation.conversationMembership?.lastReadMessageSortKey ?? null
102
- const jumpToUnreadAnchor =
103
- featureEnabled(availableFeatures.jump_to_unread) && !reply_root_id
104
- ? lastReadMessageSortKey
105
- : null
106
- const initialMessageId = message_id ?? jumpToUnreadAnchor
107
- const initialMessageIdIsAnchor = !!initialMessageId && !message_id
108
-
109
75
  return (
110
76
  <ConversationContextProvider
111
77
  conversationId={conversation_id}
112
78
  currentPageReplyRootId={reply_root_id ?? null}
113
- initialMessageId={initialMessageId}
114
- initialMessageIdIsAnchor={initialMessageIdIsAnchor}
115
79
  >
116
80
  <ConversationScreenContent route={route} />
117
81
  </ConversationContextProvider>
@@ -121,49 +85,29 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
121
85
  function ConversationScreenContent({ route }: ConversationScreenProps) {
122
86
  const styles = useStyles()
123
87
  const navigation = useNavigation()
124
- const {
125
- conversation_id: conversationId,
126
- editing_message_id: editingMessageId,
127
- reply_root_id: replyRootId,
128
- reply_root_author_name: replyRootAuthorName,
129
- } = route.params
88
+ const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
89
+ route.params
130
90
  const { data: conversation } = useConversation(route.params)
131
- const {
132
- messages,
133
- fetchOlderMessages,
134
- fetchNewerMessages,
135
- hasMoreNewerMessages,
136
- isFetchingNewerMessages,
137
- cancelFetchNewerMessages,
138
- } = useConversationMessages({ conversation_id: conversationId, reply_root_id: replyRootId })
139
-
140
- const { jumpToUnreadActive } = useJumpToUnreadGates()
141
- const { initialMessageId } = useConversationContext()
142
-
143
- useConversationJoltEvents({ conversationId })
144
- 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 })
145
97
  useEnsureConversationsRouteExists()
146
98
  useMarkLatestMessageRead({ conversation, messages })
147
- const { onMessageSeen } = useTrackHighestSeenMessage()
148
-
149
- const items = useMemo(
150
- () =>
151
- groupMessages({
152
- ms: messages,
153
- inReplyScreen: !!replyRootId,
154
- jumpToUnreadActive,
155
- initialMessageId,
156
- }),
157
- [messages, replyRootId, jumpToUnreadActive, initialMessageId]
158
- )
159
- const noMessages = items.length === 0
99
+ const messagesWithSeparators = groupMessages({
100
+ ms: messages,
101
+ inReplyScreen: !!reply_root_id,
102
+ })
103
+ const noMessages = messagesWithSeparators.length === 0
160
104
 
161
105
  const { repliesDisabled, memberAbility, badges, title } = conversation
162
106
  const canReply = memberAbility?.canReply
163
107
  const showLeaderDisabledReplyBanner = canReply && repliesDisabled
164
108
  const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
165
- const currentlyEditingMessage = messages.find(m => String(m.id) === String(editingMessageId))
166
- 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]
167
111
  const replyHeaderTitle = replyRootAuthorFirstName
168
112
  ? `Reply to ${replyRootAuthorFirstName}`
169
113
  : 'Reply'
@@ -171,96 +115,21 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
171
115
  const muted = conversation.conversationMembership?.muted ?? conversation.muted
172
116
 
173
117
  const listRef = useRef<FlatList>(null)
174
- const [dividerScrolledPast, setDividerScrolledPast] = useState(false)
175
-
176
- const observers = useMemo<ViewabilityObserver<EnrichedMessage>[]>(
177
- () => [
178
- reportViewableMessages(onMessageSeen),
179
- detectDividerExitTowardNewer({
180
- dividerKey: UNREAD_DIVIDER_KEY,
181
- initialMessageId,
182
- onExited: () => setDividerScrolledPast(true),
183
- }),
184
- ],
185
- [onMessageSeen, initialMessageId]
186
- )
118
+ const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
187
119
 
188
- const { viewabilityConfigCallbackPairs, onScrollBeginDrag: viewabilityOnScrollBeginDrag } =
189
- useFlatListViewability({ observers })
190
- const {
191
- onContentSizeChange,
192
- onScrollToIndexFailed,
193
- onScrollBeginDrag: anchorOnScrollBeginDrag,
194
- } = useJumpToUnreadAnchor({ listRef, items })
195
- const onScrollBeginDrag = useCallback(() => {
196
- viewabilityOnScrollBeginDrag()
197
- anchorOnScrollBeginDrag()
198
- }, [viewabilityOnScrollBeginDrag, anchorOnScrollBeginDrag])
199
- const { onScroll, showJumpToBottomButton } = useScrollTracking({
200
- hasMoreNewerMessages,
201
- isFetchingNewerMessages,
202
- fetchNewerMessages,
203
- cancelFetchNewerMessages,
204
- })
205
- const { handleJumpToBottom, isJumpingToBottom } = useJumpToBottomAction({ listRef })
206
-
207
- const listHeader = useMemo(
208
- () => (
209
- <View>
210
- {isFetchingNewerMessages && (
211
- <Animated.View
212
- entering={FadeIn.duration(750)}
213
- exiting={FadeOut.duration(750)}
214
- style={styles.loadingFooter}
215
- accessibilityRole="progressbar"
216
- accessibilityLabel="Loading more messages"
217
- >
218
- <ActivityIndicator />
219
- </Animated.View>
220
- )}
221
- <View style={styles.listHeader} />
222
- </View>
223
- ),
224
- [isFetchingNewerMessages, styles.loadingFooter, styles.listHeader]
225
- )
120
+ const trackScroll = (event: any) => {
121
+ const offsetY = event.nativeEvent.contentOffset.y
122
+ setShowJumpToBottomButton(offsetY > 200)
123
+ }
226
124
 
227
- const renderItem = useCallback(
228
- ({ item }: { item: EnrichedMessage }) => {
229
- if (item.type === 'DateSeparator') return <InlineDateSeparator {...item} />
230
- if (item.type === 'UnreadDivider') return <UnreadDivider scrolledPast={dividerScrolledPast} />
231
- if (item.type === 'ReplyShadowMessage') {
232
- return (
233
- <ReplyShadowMessage
234
- {...item}
235
- conversation_id={conversationId}
236
- inReplyScreen={!!replyRootId}
237
- />
238
- )
239
- }
240
- if (isSystemMessage(item)) {
241
- return <SystemMessage message={item} conversationId={conversationId} />
242
- }
243
- return (
244
- <Message
245
- {...item}
246
- canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
247
- conversation_id={conversationId}
248
- latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
249
- inReplyScreen={!!replyRootId}
250
- />
251
- )
252
- },
253
- [
254
- dividerScrolledPast,
255
- conversationId,
256
- replyRootId,
257
- canDeleteNonAuthoredMessages,
258
- conversation?.latestReadMessageSortKey,
259
- ]
260
- )
125
+ const handleReturnToBottom = useCallback(() => {
126
+ listRef.current?.scrollToOffset({
127
+ offset: 0,
128
+ })
129
+ }, [])
261
130
 
262
131
  useEffect(() => {
263
- if (replyRootId) {
132
+ if (reply_root_id) {
264
133
  navigation.setParams({
265
134
  title: replyHeaderTitle,
266
135
  })
@@ -272,7 +141,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
272
141
  muted,
273
142
  })
274
143
  }
275
- }, [navigation, title, badges, conversation?.deleted, replyRootId, replyHeaderTitle, muted])
144
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle, muted])
276
145
 
277
146
  if (!conversation || conversation.deleted) {
278
147
  return (
@@ -299,31 +168,55 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
299
168
  {noMessages ? (
300
169
  <EmptyConversationBlankState />
301
170
  ) : (
302
- <MessageList
303
- listRef={listRef}
304
- data={items}
305
- onScroll={onScroll}
306
- onScrollBeginDrag={onScrollBeginDrag}
307
- viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
308
- onContentSizeChange={onContentSizeChange}
309
- onScrollToIndexFailed={onScrollToIndexFailed}
310
- renderItem={renderItem}
311
- onEndReached={() => fetchOlderMessages()}
312
- ListHeaderComponent={listHeader}
171
+ <FlatList
172
+ inverted
173
+ ref={listRef}
174
+ contentContainerStyle={styles.listContainer}
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} />}
313
210
  />
314
211
  )}
315
- <JumpToBottomButton
316
- onPress={handleJumpToBottom}
317
- visible={showJumpToBottomButton}
318
- loading={isJumpingToBottom}
319
- />
212
+ <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
320
213
  {!noMessages && <TypingIndicator />}
321
214
  {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
322
215
  <ConversationBottomBar
323
216
  conversation={conversation}
324
217
  canReply={canReply}
325
218
  replyRootAuthorFirstName={replyRootAuthorFirstName}
326
- replyRootId={replyRootId}
219
+ replyRootId={reply_root_id}
327
220
  currentlyEditingMessage={currentlyEditingMessage}
328
221
  />
329
222
  </KeyboardView>
@@ -493,14 +386,13 @@ const useStyles = () => {
493
386
  backgroundColor: navigationTheme.colors.card,
494
387
  paddingBottom: bottom,
495
388
  },
389
+ listContainer: {
390
+ paddingVertical: 12,
391
+ },
496
392
  listHeader: {
497
393
  // Just whitespace to provide space where the typing indicator can be
498
394
  height: 16,
499
395
  },
500
- loadingFooter: {
501
- paddingVertical: 12,
502
- alignItems: 'center',
503
- },
504
396
  })
505
397
  }
506
398
 
@@ -1,4 +1,3 @@
1
- import type { AttachmentFlaggedEvent } from './attachment_events'
2
1
  import type {
3
2
  ConversationCreatedEvent,
4
3
  ConversationDeletedEvent,
@@ -21,14 +20,12 @@ export type JoltConversationEvent =
21
20
  export type JoltMessageEvent = MessageCreatedEvent | MessageUpdatedEvent | MessageDeletedEvent
22
21
  export type JoltReactionEvent = ReactionCreatedEvent | ReactionDeletedEvent
23
22
  export type JoltTypingEvent = TypingBroadcastEvent
24
- export type JoltAttachmentEvent = AttachmentFlaggedEvent
25
23
 
26
24
  export type CustomJoltEvent =
27
25
  | JoltConversationEvent
28
26
  | JoltMessageEvent
29
27
  | JoltReactionEvent
30
28
  | JoltTypingEvent
31
- | JoltAttachmentEvent
32
29
 
33
30
  export type JoltEventName = CustomJoltEvent['event'] | 'STREAM_USER_UPDATED'
34
31
  export type JoltSubscriptionPattern = JoltEventName | 'reaction.*'
@@ -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,10 +0,0 @@
1
- import { RefObject } from 'react';
2
- import { type FlatListProps } from 'react-native';
3
- import { FlatList } from 'react-native-gesture-handler';
4
- import type { EnrichedMessage } from '../../utils/group_messages';
5
- type MessageListProps = Pick<FlatListProps<EnrichedMessage>, 'data' | 'renderItem' | 'onScroll' | 'onScrollBeginDrag' | 'viewabilityConfigCallbackPairs' | 'onContentSizeChange' | 'onScrollToIndexFailed' | 'onEndReached' | 'ListHeaderComponent'> & {
6
- listRef: RefObject<FlatList | null>;
7
- };
8
- export declare function MessageList({ listRef, ...props }: MessageListProps): import("react").JSX.Element;
9
- export {};
10
- //# sourceMappingURL=message_list.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message_list.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/message_list.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAKjE,KAAK,gBAAgB,GAAG,IAAI,CAC1B,aAAa,CAAC,eAAe,CAAC,EAC5B,MAAM,GACN,YAAY,GACZ,UAAU,GACV,mBAAmB,GACnB,gCAAgC,GAChC,qBAAqB,GACrB,uBAAuB,GACvB,cAAc,GACd,qBAAqB,CACxB,GAAG;IACF,OAAO,EAAE,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;CACpC,CAAA;AAED,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,EAAE,gBAAgB,+BAYlE"}
@@ -1,13 +0,0 @@
1
- import { StyleSheet } from 'react-native';
2
- import { FlatList } from 'react-native-gesture-handler';
3
- const extractItemKey = (item) => String(item.id);
4
- const maintainVisibleContentPosition = { minIndexForVisible: 0 };
5
- export function MessageList({ listRef, ...props }) {
6
- return (<FlatList inverted ref={listRef} contentContainerStyle={styles.listContainer} maintainVisibleContentPosition={maintainVisibleContentPosition} keyExtractor={extractItemKey} scrollEventThrottle={64} {...props}/>);
7
- }
8
- const styles = StyleSheet.create({
9
- listContainer: {
10
- paddingVertical: 12,
11
- },
12
- });
13
- //# sourceMappingURL=message_list.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message_list.js","sourceRoot":"","sources":["../../../src/components/conversation/message_list.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAsB,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAA;AAGvD,MAAM,cAAc,GAAG,CAAC,IAAqB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACjE,MAAM,8BAA8B,GAAG,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAA;AAiBhE,MAAM,UAAU,WAAW,CAAC,EAAE,OAAO,EAAE,GAAG,KAAK,EAAoB;IACjE,OAAO,CACL,CAAC,QAAQ,CACP,QAAQ,CACR,GAAG,CAAC,CAAC,OAAO,CAAC,CACb,qBAAqB,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAC5C,8BAA8B,CAAC,CAAC,8BAA8B,CAAC,CAC/D,YAAY,CAAC,CAAC,cAAc,CAAC,CAC7B,mBAAmB,CAAC,CAAC,EAAE,CAAC,CACxB,IAAI,KAAK,CAAC,EACV,CACH,CAAA;AACH,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC/B,aAAa,EAAE;QACb,eAAe,EAAE,EAAE;KACpB;CACF,CAAC,CAAA","sourcesContent":["import { RefObject } from 'react'\nimport { StyleSheet, type FlatListProps } from 'react-native'\nimport { FlatList } from 'react-native-gesture-handler'\nimport type { EnrichedMessage } from '../../utils/group_messages'\n\nconst extractItemKey = (item: EnrichedMessage) => String(item.id)\nconst maintainVisibleContentPosition = { minIndexForVisible: 0 }\n\ntype MessageListProps = Pick<\n FlatListProps<EnrichedMessage>,\n | 'data'\n | 'renderItem'\n | 'onScroll'\n | 'onScrollBeginDrag'\n | 'viewabilityConfigCallbackPairs'\n | 'onContentSizeChange'\n | 'onScrollToIndexFailed'\n | 'onEndReached'\n | 'ListHeaderComponent'\n> & {\n listRef: RefObject<FlatList | null>\n}\n\nexport function MessageList({ listRef, ...props }: MessageListProps) {\n return (\n <FlatList\n inverted\n ref={listRef}\n contentContainerStyle={styles.listContainer}\n maintainVisibleContentPosition={maintainVisibleContentPosition}\n keyExtractor={extractItemKey}\n scrollEventThrottle={64}\n {...props}\n />\n )\n}\n\nconst styles = StyleSheet.create({\n listContainer: {\n paddingVertical: 12,\n },\n})\n"]}
@@ -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"}