@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.
- 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 +45 -96
- 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 +2 -2
- 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 +69 -186
- 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,9 +8,8 @@ import {
|
|
|
8
8
|
useTheme as useNavigationTheme,
|
|
9
9
|
useRoute,
|
|
10
10
|
} from '@react-navigation/native'
|
|
11
|
-
import React, { useCallback, useEffect,
|
|
12
|
-
import {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
167
|
-
const replyRootAuthorFirstName =
|
|
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 [
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
229
|
-
({
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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={
|
|
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 +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
|