@planningcenter/chat-react-native 3.38.0-rc.10 → 3.38.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/conversation/jump_to_bottom_button.d.ts +1 -2
- package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
- package/build/components/conversation/jump_to_bottom_button.js +7 -39
- package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts +2 -1
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/contexts/conversation_context.d.ts +1 -8
- package/build/contexts/conversation_context.d.ts.map +1 -1
- package/build/contexts/conversation_context.js +3 -21
- package/build/contexts/conversation_context.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts +6 -15
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +9 -62
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +4 -4
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_conversations_actions.d.ts +0 -5
- package/build/hooks/use_conversations_actions.d.ts.map +1 -1
- package/build/hooks/use_conversations_actions.js +0 -12
- package/build/hooks/use_conversations_actions.js.map +1 -1
- package/build/hooks/use_features.d.ts +0 -1
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +0 -1
- package/build/hooks/use_features.js.map +1 -1
- package/build/hooks/use_mark_latest_message_read.d.ts +1 -1
- package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
- package/build/hooks/use_mark_latest_message_read.js +1 -17
- package/build/hooks/use_mark_latest_message_read.js.map +1 -1
- package/build/hooks/use_suspense_api.d.ts +0 -1
- package/build/hooks/use_suspense_api.d.ts.map +1 -1
- package/build/hooks/use_suspense_api.js +1 -1
- package/build/hooks/use_suspense_api.js.map +1 -1
- package/build/screens/conversation_screen.d.ts +0 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +48 -95
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +0 -1
- package/build/utils/cache/messages_cache.d.ts.map +1 -1
- package/build/utils/cache/messages_cache.js +0 -4
- package/build/utils/cache/messages_cache.js.map +1 -1
- package/build/utils/group_messages.d.ts +2 -9
- package/build/utils/group_messages.d.ts.map +1 -1
- package/build/utils/group_messages.js +1 -20
- package/build/utils/group_messages.js.map +1 -1
- package/package.json +3 -3
- package/src/components/conversation/jump_to_bottom_button.tsx +8 -57
- package/src/components/conversation/reply_shadow_message.tsx +1 -1
- package/src/contexts/conversation_context.tsx +2 -30
- package/src/hooks/use_conversation_messages.ts +20 -120
- package/src/hooks/use_conversation_messages_jolt_events.ts +3 -4
- package/src/hooks/use_conversations_actions.ts +0 -15
- package/src/hooks/use_features.ts +0 -1
- package/src/hooks/use_mark_latest_message_read.ts +2 -16
- package/src/hooks/use_suspense_api.ts +1 -1
- package/src/screens/conversation_screen.tsx +76 -184
- package/src/utils/__tests__/group_messages.test.ts +0 -71
- package/src/utils/cache/messages_cache.ts +0 -5
- package/src/utils/group_messages.ts +2 -42
- package/build/components/conversation/unread_divider.d.ts +0 -6
- package/build/components/conversation/unread_divider.d.ts.map +0 -1
- package/build/components/conversation/unread_divider.js +0 -59
- package/build/components/conversation/unread_divider.js.map +0 -1
- package/build/hooks/use_flat_list_viewability.d.ts +0 -20
- package/build/hooks/use_flat_list_viewability.d.ts.map +0 -1
- package/build/hooks/use_flat_list_viewability.js +0 -30
- package/build/hooks/use_flat_list_viewability.js.map +0 -1
- package/build/hooks/use_jump_to_bottom_action.d.ts +0 -9
- package/build/hooks/use_jump_to_bottom_action.d.ts.map +0 -1
- package/build/hooks/use_jump_to_bottom_action.js +0 -62
- package/build/hooks/use_jump_to_bottom_action.js.map +0 -1
- package/build/hooks/use_jump_to_unread_anchor.d.ts +0 -20
- package/build/hooks/use_jump_to_unread_anchor.d.ts.map +0 -1
- package/build/hooks/use_jump_to_unread_anchor.js +0 -53
- package/build/hooks/use_jump_to_unread_anchor.js.map +0 -1
- package/build/hooks/use_jump_to_unread_gates.d.ts +0 -5
- package/build/hooks/use_jump_to_unread_gates.d.ts.map +0 -1
- package/build/hooks/use_jump_to_unread_gates.js +0 -10
- package/build/hooks/use_jump_to_unread_gates.js.map +0 -1
- package/build/hooks/use_scroll_tracking.d.ts +0 -13
- package/build/hooks/use_scroll_tracking.d.ts.map +0 -1
- package/build/hooks/use_scroll_tracking.js +0 -45
- package/build/hooks/use_scroll_tracking.js.map +0 -1
- package/build/hooks/use_track_highest_seen_message.d.ts +0 -4
- package/build/hooks/use_track_highest_seen_message.d.ts.map +0 -1
- package/build/hooks/use_track_highest_seen_message.js +0 -35
- package/build/hooks/use_track_highest_seen_message.js.map +0 -1
- package/build/utils/conversation_messages.d.ts +0 -10
- package/build/utils/conversation_messages.d.ts.map +0 -1
- package/build/utils/conversation_messages.js +0 -22
- package/build/utils/conversation_messages.js.map +0 -1
- package/build/utils/highest_seen_tracker.d.ts +0 -12
- package/build/utils/highest_seen_tracker.d.ts.map +0 -1
- package/build/utils/highest_seen_tracker.js +0 -37
- package/build/utils/highest_seen_tracker.js.map +0 -1
- package/build/utils/message_viewability.d.ts +0 -24
- package/build/utils/message_viewability.d.ts.map +0 -1
- package/build/utils/message_viewability.js +0 -29
- package/build/utils/message_viewability.js.map +0 -1
- package/build/utils/unread_divider_helpers.d.ts +0 -18
- package/build/utils/unread_divider_helpers.d.ts.map +0 -1
- package/build/utils/unread_divider_helpers.js +0 -13
- package/build/utils/unread_divider_helpers.js.map +0 -1
- package/src/__tests__/hooks/use_conversation_messages.test.tsx +0 -109
- package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +0 -154
- package/src/__tests__/utils/cache/messages_cache.test.ts +0 -54
- package/src/components/conversation/unread_divider.tsx +0 -90
- package/src/hooks/use_flat_list_viewability.ts +0 -50
- package/src/hooks/use_jump_to_bottom_action.ts +0 -75
- package/src/hooks/use_jump_to_unread_anchor.ts +0 -68
- package/src/hooks/use_jump_to_unread_gates.ts +0 -10
- package/src/hooks/use_scroll_tracking.ts +0 -64
- package/src/hooks/use_track_highest_seen_message.ts +0 -43
- package/src/utils/__tests__/conversation_messages.test.ts +0 -105
- package/src/utils/__tests__/highest_seen_tracker.test.ts +0 -82
- package/src/utils/__tests__/message_viewability.test.ts +0 -168
- package/src/utils/__tests__/unread_divider_helpers.test.ts +0 -85
- package/src/utils/conversation_messages.ts +0 -37
- package/src/utils/highest_seen_tracker.ts +0 -42
- package/src/utils/message_viewability.ts +0 -49
- 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
|
|
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
|
-
|
|
93
|
+
const throwResponseError = (error: unknown) => {
|
|
94
94
|
if (error instanceof Response) {
|
|
95
95
|
throw new ResponseError(error as FailedResponse)
|
|
96
96
|
}
|
|
@@ -8,17 +8,14 @@ import {
|
|
|
8
8
|
useTheme as useNavigationTheme,
|
|
9
9
|
useRoute,
|
|
10
10
|
} from '@react-navigation/native'
|
|
11
|
-
import React, { useCallback, useEffect,
|
|
12
|
-
import {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
166
|
-
const replyRootAuthorFirstName =
|
|
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 [
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
228
|
-
({
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
renderItem={
|
|
311
|
-
|
|
312
|
-
|
|
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={
|
|
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
|
|
|
@@ -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 +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
|