@planningcenter/chat-react-native 3.35.0-rc.2 → 3.35.0-rc.4
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/README.md +1 -1
- package/build/components/conversation/jump_to_bottom_button.d.ts +2 -1
- package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
- package/build/components/conversation/jump_to_bottom_button.js +39 -7
- package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts +1 -2
- 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/components/conversation/unread_divider.d.ts +6 -0
- package/build/components/conversation/unread_divider.d.ts.map +1 -0
- package/build/components/conversation/unread_divider.js +59 -0
- package/build/components/conversation/unread_divider.js.map +1 -0
- package/build/contexts/conversation_context.d.ts +2 -0
- package/build/contexts/conversation_context.d.ts.map +1 -1
- package/build/contexts/conversation_context.js +13 -5
- package/build/contexts/conversation_context.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts +2 -0
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +9 -5
- 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 +5 -0
- package/build/hooks/use_conversations_actions.d.ts.map +1 -1
- package/build/hooks/use_conversations_actions.js +12 -0
- package/build/hooks/use_conversations_actions.js.map +1 -1
- package/build/hooks/use_flat_list_viewability.d.ts +20 -0
- package/build/hooks/use_flat_list_viewability.d.ts.map +1 -0
- package/build/hooks/use_flat_list_viewability.js +30 -0
- package/build/hooks/use_flat_list_viewability.js.map +1 -0
- package/build/hooks/use_jump_to_bottom_action.d.ts +9 -0
- package/build/hooks/use_jump_to_bottom_action.d.ts.map +1 -0
- package/build/hooks/use_jump_to_bottom_action.js +62 -0
- package/build/hooks/use_jump_to_bottom_action.js.map +1 -0
- package/build/hooks/use_jump_to_unread_anchor.d.ts +20 -0
- package/build/hooks/use_jump_to_unread_anchor.d.ts.map +1 -0
- package/build/hooks/use_jump_to_unread_anchor.js +53 -0
- package/build/hooks/use_jump_to_unread_anchor.js.map +1 -0
- package/build/hooks/use_jump_to_unread_gates.d.ts +5 -0
- package/build/hooks/use_jump_to_unread_gates.d.ts.map +1 -0
- package/build/hooks/use_jump_to_unread_gates.js +10 -0
- package/build/hooks/use_jump_to_unread_gates.js.map +1 -0
- 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 +17 -1
- package/build/hooks/use_mark_latest_message_read.js.map +1 -1
- package/build/hooks/use_scroll_tracking.d.ts +13 -0
- package/build/hooks/use_scroll_tracking.d.ts.map +1 -0
- package/build/hooks/use_scroll_tracking.js +45 -0
- package/build/hooks/use_scroll_tracking.js.map +1 -0
- package/build/hooks/use_track_highest_seen_message.d.ts +4 -0
- package/build/hooks/use_track_highest_seen_message.d.ts.map +1 -0
- package/build/hooks/use_track_highest_seen_message.js +35 -0
- package/build/hooks/use_track_highest_seen_message.js.map +1 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/screens/conversation_screen.d.ts +0 -19
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +87 -139
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +1 -0
- package/build/utils/cache/messages_cache.d.ts.map +1 -1
- package/build/utils/cache/messages_cache.js +4 -0
- package/build/utils/cache/messages_cache.js.map +1 -1
- package/build/utils/group_messages.d.ts +28 -0
- package/build/utils/group_messages.d.ts.map +1 -0
- package/build/utils/group_messages.js +142 -0
- package/build/utils/group_messages.js.map +1 -0
- package/build/utils/highest_seen_tracker.d.ts +12 -0
- package/build/utils/highest_seen_tracker.d.ts.map +1 -0
- package/build/utils/highest_seen_tracker.js +37 -0
- package/build/utils/highest_seen_tracker.js.map +1 -0
- package/build/utils/message_viewability.d.ts +24 -0
- package/build/utils/message_viewability.d.ts.map +1 -0
- package/build/utils/message_viewability.js +29 -0
- package/build/utils/message_viewability.js.map +1 -0
- package/build/utils/unread_divider_helpers.d.ts +18 -0
- package/build/utils/unread_divider_helpers.d.ts.map +1 -0
- package/build/utils/unread_divider_helpers.js +13 -0
- package/build/utils/unread_divider_helpers.js.map +1 -0
- package/package.json +10 -4
- package/src/__tests__/contexts/session_context.tsx +1 -1
- package/src/__tests__/hooks/use_async_storage.test.tsx +1 -1
- package/src/__tests__/hooks/use_attachment_uploader.test.tsx +1 -1
- package/src/__tests__/hooks/use_chat_configuration.test.tsx +1 -1
- package/src/__tests__/hooks/use_conversation_messages.test.tsx +1 -1
- package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +154 -0
- package/src/__tests__/utils/cache/messages_cache.test.ts +54 -0
- package/src/components/conversation/jump_to_bottom_button.tsx +57 -8
- package/src/components/conversation/reply_shadow_message.tsx +4 -2
- package/src/components/conversation/unread_divider.tsx +90 -0
- package/src/contexts/conversation_context.tsx +15 -13
- package/src/hooks/use_conversation_messages.ts +19 -3
- package/src/hooks/use_conversation_messages_jolt_events.ts +4 -3
- package/src/hooks/use_conversations_actions.ts +15 -0
- package/src/hooks/use_flat_list_viewability.ts +50 -0
- package/src/hooks/use_jump_to_bottom_action.ts +75 -0
- package/src/hooks/use_jump_to_unread_anchor.ts +68 -0
- package/src/hooks/use_jump_to_unread_gates.ts +10 -0
- package/src/hooks/use_mark_latest_message_read.ts +16 -2
- package/src/hooks/use_scroll_tracking.ts +64 -0
- package/src/hooks/use_track_highest_seen_message.ts +43 -0
- package/src/screens/conversation_screen.tsx +173 -197
- package/src/utils/__tests__/group_messages.test.ts +214 -0
- package/src/utils/__tests__/highest_seen_tracker.test.ts +82 -0
- package/src/utils/__tests__/message_viewability.test.ts +168 -0
- package/src/utils/__tests__/unread_divider_helpers.test.ts +85 -0
- package/src/utils/cache/messages_cache.ts +5 -0
- package/src/utils/group_messages.ts +217 -0
- package/src/utils/highest_seen_tracker.ts +42 -0
- package/src/utils/message_viewability.ts +49 -0
- package/src/utils/unread_divider_helpers.ts +25 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'
|
|
3
|
+
import { useConversationContext } from '../contexts/conversation_context'
|
|
4
|
+
|
|
5
|
+
const JUMP_TO_BOTTOM_OFFSET_THRESHOLD = 200
|
|
6
|
+
const AT_BOTTOM_OFFSET_TOLERANCE = 5
|
|
7
|
+
const FETCH_NEWER_OFFSET_THRESHOLD = 600
|
|
8
|
+
|
|
9
|
+
interface UseScrollTrackingArgs {
|
|
10
|
+
hasMoreNewerMessages: boolean
|
|
11
|
+
isFetchingNewerMessages: boolean
|
|
12
|
+
fetchNewerMessages: () => void
|
|
13
|
+
cancelFetchNewerMessages: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useScrollTracking({
|
|
17
|
+
hasMoreNewerMessages,
|
|
18
|
+
isFetchingNewerMessages,
|
|
19
|
+
fetchNewerMessages,
|
|
20
|
+
cancelFetchNewerMessages,
|
|
21
|
+
}: UseScrollTrackingArgs) {
|
|
22
|
+
const { atEndOfMessageHistory, setAtEndOfMessageHistory } = useConversationContext()
|
|
23
|
+
const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
|
|
24
|
+
const withinBoundaryRef = useRef(false)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
return () => {
|
|
28
|
+
cancelFetchNewerMessages()
|
|
29
|
+
}
|
|
30
|
+
}, [cancelFetchNewerMessages])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isFetchingNewerMessages) withinBoundaryRef.current = false
|
|
34
|
+
}, [isFetchingNewerMessages])
|
|
35
|
+
|
|
36
|
+
const onScroll = useCallback(
|
|
37
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
38
|
+
const offsetY = event.nativeEvent.contentOffset.y
|
|
39
|
+
setShowJumpToBottomButton(offsetY > JUMP_TO_BOTTOM_OFFSET_THRESHOLD)
|
|
40
|
+
|
|
41
|
+
const atBottom = offsetY < AT_BOTTOM_OFFSET_TOLERANCE
|
|
42
|
+
const atEnd = atBottom && !hasMoreNewerMessages
|
|
43
|
+
if (atEnd !== atEndOfMessageHistory) setAtEndOfMessageHistory(atEnd)
|
|
44
|
+
|
|
45
|
+
if (offsetY >= FETCH_NEWER_OFFSET_THRESHOLD) {
|
|
46
|
+
withinBoundaryRef.current = false
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
if (withinBoundaryRef.current) return
|
|
50
|
+
if (!hasMoreNewerMessages || isFetchingNewerMessages) return
|
|
51
|
+
withinBoundaryRef.current = true
|
|
52
|
+
fetchNewerMessages()
|
|
53
|
+
},
|
|
54
|
+
[
|
|
55
|
+
hasMoreNewerMessages,
|
|
56
|
+
isFetchingNewerMessages,
|
|
57
|
+
fetchNewerMessages,
|
|
58
|
+
atEndOfMessageHistory,
|
|
59
|
+
setAtEndOfMessageHistory,
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return { onScroll, showJumpToBottomButton }
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo } from 'react'
|
|
2
|
+
import { AppState } from 'react-native'
|
|
3
|
+
import { useConversationContext } from '../contexts/conversation_context'
|
|
4
|
+
import { makeHighestSeenTracker } from '../utils/highest_seen_tracker'
|
|
5
|
+
import { useConversationsMarkReadUpTo } from './use_conversations_actions'
|
|
6
|
+
import { useJumpToUnreadGates } from './use_jump_to_unread_gates'
|
|
7
|
+
|
|
8
|
+
export function useTrackHighestSeenMessage() {
|
|
9
|
+
const { conversationId, currentPageReplyRootId } = useConversationContext()
|
|
10
|
+
const { jumpToUnreadActive } = useJumpToUnreadGates()
|
|
11
|
+
const enabled = jumpToUnreadActive && !currentPageReplyRootId
|
|
12
|
+
const { mutate: markReadUpTo } = useConversationsMarkReadUpTo({ conversationId })
|
|
13
|
+
|
|
14
|
+
const tracker = useMemo(
|
|
15
|
+
() => makeHighestSeenTracker(conversationId, ({ sortKey }) => markReadUpTo({ sortKey })),
|
|
16
|
+
[conversationId, markReadUpTo]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const onMessageSeen = useCallback(
|
|
20
|
+
(sortKey: string) => {
|
|
21
|
+
if (!enabled) return
|
|
22
|
+
tracker.onSeen(sortKey)
|
|
23
|
+
},
|
|
24
|
+
[enabled, tracker]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!enabled) return
|
|
29
|
+
const sub = AppState.addEventListener('change', state => {
|
|
30
|
+
if (state !== 'active') tracker.flushNow()
|
|
31
|
+
})
|
|
32
|
+
return () => sub.remove()
|
|
33
|
+
}, [enabled, tracker])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
return () => {
|
|
37
|
+
tracker.flushNow()
|
|
38
|
+
tracker.cancel()
|
|
39
|
+
}
|
|
40
|
+
}, [tracker])
|
|
41
|
+
|
|
42
|
+
return { onMessageSeen }
|
|
43
|
+
}
|
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
useTheme as useNavigationTheme,
|
|
9
9
|
useRoute,
|
|
10
10
|
} from '@react-navigation/native'
|
|
11
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
12
|
-
import { FlatList, Platform, StyleSheet, View } from 'react-native'
|
|
11
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
12
|
+
import { ActivityIndicator, FlatList, Platform, StyleSheet, View } from 'react-native'
|
|
13
|
+
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
|
13
14
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
14
15
|
import { Badge, Icon, Text } from '../components'
|
|
15
16
|
import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
|
|
@@ -23,24 +24,43 @@ import {
|
|
|
23
24
|
import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
|
|
24
25
|
import { SystemMessage } from '../components/conversation/system_message'
|
|
25
26
|
import { TypingIndicator } from '../components/conversation/typing_indicator'
|
|
27
|
+
import { UnreadDivider } from '../components/conversation/unread_divider'
|
|
26
28
|
import { KeyboardView } from '../components/display/keyboard_view'
|
|
27
29
|
import BlankState from '../components/primitive/blank_state_primitive'
|
|
28
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
ConversationContextProvider,
|
|
32
|
+
useConversationContext,
|
|
33
|
+
} from '../contexts/conversation_context'
|
|
29
34
|
import { useTheme } from '../hooks'
|
|
30
35
|
import { useConversation } from '../hooks/use_conversation'
|
|
31
36
|
import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
|
|
32
37
|
import { useConversationMessages } from '../hooks/use_conversation_messages'
|
|
33
38
|
import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
|
|
34
39
|
import { useFeatures } from '../hooks/use_features'
|
|
40
|
+
import { useFlatListViewability } from '../hooks/use_flat_list_viewability'
|
|
41
|
+
import { useJumpToBottomAction } from '../hooks/use_jump_to_bottom_action'
|
|
42
|
+
import { useJumpToUnreadAnchor } from '../hooks/use_jump_to_unread_anchor'
|
|
43
|
+
import { useJumpToUnreadGates } from '../hooks/use_jump_to_unread_gates'
|
|
35
44
|
import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
|
|
36
45
|
import {
|
|
37
46
|
normalizeAnalyticsMetadata,
|
|
38
47
|
usePublishProductAnalyticsEvent,
|
|
39
48
|
} from '../hooks/use_product_analytics'
|
|
40
|
-
import {
|
|
49
|
+
import { useScrollTracking } from '../hooks/use_scroll_tracking'
|
|
50
|
+
import { useTrackHighestSeenMessage } from '../hooks/use_track_highest_seen_message'
|
|
41
51
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge'
|
|
42
52
|
import { getRelativeDateStatus } from '../utils/date'
|
|
43
|
-
import
|
|
53
|
+
import {
|
|
54
|
+
groupMessages,
|
|
55
|
+
UNREAD_DIVIDER_KEY,
|
|
56
|
+
type DateSeparator,
|
|
57
|
+
type EnrichedMessage,
|
|
58
|
+
} from '../utils/group_messages'
|
|
59
|
+
import {
|
|
60
|
+
detectDividerExitTowardNewer,
|
|
61
|
+
reportViewableMessages,
|
|
62
|
+
type ViewabilityObserver,
|
|
63
|
+
} from '../utils/message_viewability'
|
|
44
64
|
import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
|
|
45
65
|
import { isSystemMessage } from '../utils/system_messages'
|
|
46
66
|
|
|
@@ -61,6 +81,9 @@ export type ConversationRouteProps = {
|
|
|
61
81
|
|
|
62
82
|
export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
|
|
63
83
|
|
|
84
|
+
const extractItemKey = (item: EnrichedMessage) => String(item.id)
|
|
85
|
+
const maintainVisibleContentPosition = { minIndexForVisible: 0 }
|
|
86
|
+
|
|
64
87
|
export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
65
88
|
const { conversation_id, message_id, reply_root_id } = route.params
|
|
66
89
|
|
|
@@ -73,7 +96,8 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
73
96
|
})
|
|
74
97
|
|
|
75
98
|
const lastReadMessageSortKey = conversation.conversationMembership?.lastReadMessageSortKey ?? null
|
|
76
|
-
const jumpToUnreadAnchor =
|
|
99
|
+
const jumpToUnreadAnchor =
|
|
100
|
+
featureEnabled('jump_to_unread') && !reply_root_id ? lastReadMessageSortKey : null
|
|
77
101
|
const initialMessageId = message_id ?? jumpToUnreadAnchor
|
|
78
102
|
const initialMessageIdIsAnchor = !!initialMessageId && !message_id
|
|
79
103
|
|
|
@@ -92,29 +116,49 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
92
116
|
function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
93
117
|
const styles = useStyles()
|
|
94
118
|
const navigation = useNavigation()
|
|
95
|
-
const {
|
|
96
|
-
|
|
119
|
+
const {
|
|
120
|
+
conversation_id: conversationId,
|
|
121
|
+
editing_message_id: editingMessageId,
|
|
122
|
+
reply_root_id: replyRootId,
|
|
123
|
+
reply_root_author_name: replyRootAuthorName,
|
|
124
|
+
} = route.params
|
|
97
125
|
const { data: conversation } = useConversation(route.params)
|
|
98
|
-
const {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
const {
|
|
127
|
+
messages,
|
|
128
|
+
fetchOlderMessages,
|
|
129
|
+
fetchNewerMessages,
|
|
130
|
+
hasMoreNewerMessages,
|
|
131
|
+
isFetchingNewerMessages,
|
|
132
|
+
cancelFetchNewerMessages,
|
|
133
|
+
} = useConversationMessages({ conversation_id: conversationId, reply_root_id: replyRootId })
|
|
134
|
+
|
|
135
|
+
const { jumpToUnreadActive } = useJumpToUnreadGates()
|
|
136
|
+
const { initialMessageId } = useConversationContext()
|
|
137
|
+
|
|
138
|
+
useConversationJoltEvents({ conversationId })
|
|
139
|
+
useConversationMessagesJoltEvents({ conversationId })
|
|
104
140
|
useEnsureConversationsRouteExists()
|
|
105
141
|
useMarkLatestMessageRead({ conversation, messages })
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
const { onMessageSeen } = useTrackHighestSeenMessage()
|
|
143
|
+
|
|
144
|
+
const items = useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
groupMessages({
|
|
147
|
+
ms: messages,
|
|
148
|
+
inReplyScreen: !!replyRootId,
|
|
149
|
+
jumpToUnreadActive,
|
|
150
|
+
initialMessageId,
|
|
151
|
+
}),
|
|
152
|
+
[messages, replyRootId, jumpToUnreadActive, initialMessageId]
|
|
153
|
+
)
|
|
154
|
+
const noMessages = items.length === 0
|
|
111
155
|
|
|
112
156
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
113
157
|
const canReply = memberAbility?.canReply
|
|
114
158
|
const showLeaderDisabledReplyBanner = canReply && repliesDisabled
|
|
115
159
|
const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
|
|
116
|
-
const currentlyEditingMessage = messages.find(m => String(m.id) === String(
|
|
117
|
-
const replyRootAuthorFirstName =
|
|
160
|
+
const currentlyEditingMessage = messages.find(m => String(m.id) === String(editingMessageId))
|
|
161
|
+
const replyRootAuthorFirstName = replyRootAuthorName?.split(' ')[0]
|
|
118
162
|
const replyHeaderTitle = replyRootAuthorFirstName
|
|
119
163
|
? `Reply to ${replyRootAuthorFirstName}`
|
|
120
164
|
: 'Reply'
|
|
@@ -122,21 +166,96 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
122
166
|
const muted = conversation.conversationMembership?.muted ?? conversation.muted
|
|
123
167
|
|
|
124
168
|
const listRef = useRef<FlatList>(null)
|
|
125
|
-
const [
|
|
169
|
+
const [dividerScrolledPast, setDividerScrolledPast] = useState(false)
|
|
170
|
+
|
|
171
|
+
const observers = useMemo<ViewabilityObserver<EnrichedMessage>[]>(
|
|
172
|
+
() => [
|
|
173
|
+
reportViewableMessages(onMessageSeen),
|
|
174
|
+
detectDividerExitTowardNewer({
|
|
175
|
+
dividerKey: UNREAD_DIVIDER_KEY,
|
|
176
|
+
initialMessageId,
|
|
177
|
+
onExited: () => setDividerScrolledPast(true),
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
[onMessageSeen, initialMessageId]
|
|
181
|
+
)
|
|
126
182
|
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
183
|
+
const { viewabilityConfigCallbackPairs, onScrollBeginDrag: viewabilityOnScrollBeginDrag } =
|
|
184
|
+
useFlatListViewability({ observers })
|
|
185
|
+
const {
|
|
186
|
+
onContentSizeChange,
|
|
187
|
+
onScrollToIndexFailed,
|
|
188
|
+
onScrollBeginDrag: anchorOnScrollBeginDrag,
|
|
189
|
+
} = useJumpToUnreadAnchor({ listRef, items })
|
|
190
|
+
const onScrollBeginDrag = useCallback(() => {
|
|
191
|
+
viewabilityOnScrollBeginDrag()
|
|
192
|
+
anchorOnScrollBeginDrag()
|
|
193
|
+
}, [viewabilityOnScrollBeginDrag, anchorOnScrollBeginDrag])
|
|
194
|
+
const { onScroll, showJumpToBottomButton } = useScrollTracking({
|
|
195
|
+
hasMoreNewerMessages,
|
|
196
|
+
isFetchingNewerMessages,
|
|
197
|
+
fetchNewerMessages,
|
|
198
|
+
cancelFetchNewerMessages,
|
|
199
|
+
})
|
|
200
|
+
const { handleJumpToBottom, isJumpingToBottom } = useJumpToBottomAction({ listRef })
|
|
201
|
+
|
|
202
|
+
const listHeader = useMemo(
|
|
203
|
+
() => (
|
|
204
|
+
<View>
|
|
205
|
+
{isFetchingNewerMessages && (
|
|
206
|
+
<Animated.View
|
|
207
|
+
entering={FadeIn.duration(750)}
|
|
208
|
+
exiting={FadeOut.duration(750)}
|
|
209
|
+
style={styles.loadingFooter}
|
|
210
|
+
accessibilityRole="progressbar"
|
|
211
|
+
accessibilityLabel="Loading more messages"
|
|
212
|
+
>
|
|
213
|
+
<ActivityIndicator />
|
|
214
|
+
</Animated.View>
|
|
215
|
+
)}
|
|
216
|
+
<View style={styles.listHeader} />
|
|
217
|
+
</View>
|
|
218
|
+
),
|
|
219
|
+
[isFetchingNewerMessages, styles.loadingFooter, styles.listHeader]
|
|
220
|
+
)
|
|
131
221
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
222
|
+
const renderItem = useCallback(
|
|
223
|
+
({ item }: { item: EnrichedMessage }) => {
|
|
224
|
+
if (item.type === 'DateSeparator') return <InlineDateSeparator {...item} />
|
|
225
|
+
if (item.type === 'UnreadDivider') return <UnreadDivider scrolledPast={dividerScrolledPast} />
|
|
226
|
+
if (item.type === 'ReplyShadowMessage') {
|
|
227
|
+
return (
|
|
228
|
+
<ReplyShadowMessage
|
|
229
|
+
{...item}
|
|
230
|
+
conversation_id={conversationId}
|
|
231
|
+
inReplyScreen={!!replyRootId}
|
|
232
|
+
/>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
if (isSystemMessage(item)) {
|
|
236
|
+
return <SystemMessage message={item} conversationId={conversationId} />
|
|
237
|
+
}
|
|
238
|
+
return (
|
|
239
|
+
<Message
|
|
240
|
+
{...item}
|
|
241
|
+
canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
|
|
242
|
+
conversation_id={conversationId}
|
|
243
|
+
latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
|
|
244
|
+
inReplyScreen={!!replyRootId}
|
|
245
|
+
/>
|
|
246
|
+
)
|
|
247
|
+
},
|
|
248
|
+
[
|
|
249
|
+
dividerScrolledPast,
|
|
250
|
+
conversationId,
|
|
251
|
+
replyRootId,
|
|
252
|
+
canDeleteNonAuthoredMessages,
|
|
253
|
+
conversation?.latestReadMessageSortKey,
|
|
254
|
+
]
|
|
255
|
+
)
|
|
137
256
|
|
|
138
257
|
useEffect(() => {
|
|
139
|
-
if (
|
|
258
|
+
if (replyRootId) {
|
|
140
259
|
navigation.setParams({
|
|
141
260
|
title: replyHeaderTitle,
|
|
142
261
|
})
|
|
@@ -148,7 +267,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
148
267
|
muted,
|
|
149
268
|
})
|
|
150
269
|
}
|
|
151
|
-
}, [navigation, title, badges, conversation?.deleted,
|
|
270
|
+
}, [navigation, title, badges, conversation?.deleted, replyRootId, replyHeaderTitle, muted])
|
|
152
271
|
|
|
153
272
|
if (!conversation || conversation.deleted) {
|
|
154
273
|
return (
|
|
@@ -179,53 +298,32 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
179
298
|
inverted
|
|
180
299
|
ref={listRef}
|
|
181
300
|
contentContainerStyle={styles.listContainer}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
scrollEventThrottle={
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (item.type === 'ReplyShadowMessage') {
|
|
194
|
-
return (
|
|
195
|
-
<ReplyShadowMessage
|
|
196
|
-
{...item}
|
|
197
|
-
conversation_id={conversation_id}
|
|
198
|
-
inReplyScreen={!!reply_root_id}
|
|
199
|
-
/>
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (isSystemMessage(item)) {
|
|
204
|
-
return <SystemMessage message={item} conversationId={conversation_id} />
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<Message
|
|
209
|
-
{...item}
|
|
210
|
-
canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
|
|
211
|
-
conversation_id={conversation_id}
|
|
212
|
-
latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
|
|
213
|
-
inReplyScreen={!!reply_root_id}
|
|
214
|
-
/>
|
|
215
|
-
)
|
|
216
|
-
}}
|
|
301
|
+
maintainVisibleContentPosition={maintainVisibleContentPosition}
|
|
302
|
+
data={items}
|
|
303
|
+
keyExtractor={extractItemKey}
|
|
304
|
+
onScroll={onScroll}
|
|
305
|
+
onScrollBeginDrag={onScrollBeginDrag}
|
|
306
|
+
scrollEventThrottle={64}
|
|
307
|
+
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
|
|
308
|
+
onContentSizeChange={onContentSizeChange}
|
|
309
|
+
onScrollToIndexFailed={onScrollToIndexFailed}
|
|
310
|
+
renderItem={renderItem}
|
|
217
311
|
onEndReached={() => fetchOlderMessages()}
|
|
218
|
-
ListHeaderComponent={
|
|
312
|
+
ListHeaderComponent={listHeader}
|
|
219
313
|
/>
|
|
220
314
|
)}
|
|
221
|
-
<JumpToBottomButton
|
|
315
|
+
<JumpToBottomButton
|
|
316
|
+
onPress={handleJumpToBottom}
|
|
317
|
+
visible={showJumpToBottomButton}
|
|
318
|
+
loading={isJumpingToBottom}
|
|
319
|
+
/>
|
|
222
320
|
{!noMessages && <TypingIndicator />}
|
|
223
321
|
{showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
|
|
224
322
|
{canReply ? (
|
|
225
323
|
<MessageForm.Root
|
|
226
324
|
replyRootAuthorFirstName={replyRootAuthorFirstName}
|
|
227
325
|
conversation={conversation}
|
|
228
|
-
replyRootId={
|
|
326
|
+
replyRootId={replyRootId}
|
|
229
327
|
currentlyEditingMessage={currentlyEditingMessage}
|
|
230
328
|
// We use a separate key so that it remounts component when switching between new
|
|
231
329
|
// and edit message. This simplifies internal state handling.
|
|
@@ -248,8 +346,6 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
248
346
|
)
|
|
249
347
|
}
|
|
250
348
|
|
|
251
|
-
export type DateSeparator = { type: 'DateSeparator'; id: string; date: string }
|
|
252
|
-
|
|
253
349
|
function InlineDateSeparator({ date }: DateSeparator) {
|
|
254
350
|
const styles = useDateSeparatorStyles()
|
|
255
351
|
const { isThisYear } = getRelativeDateStatus(date)
|
|
@@ -288,130 +384,6 @@ const useDateSeparatorStyles = () => {
|
|
|
288
384
|
})
|
|
289
385
|
}
|
|
290
386
|
|
|
291
|
-
type ReplyShadowMessage = {
|
|
292
|
-
type: 'ReplyShadowMessage'
|
|
293
|
-
id: string
|
|
294
|
-
messageId: string
|
|
295
|
-
isReplyShadowMessage: boolean
|
|
296
|
-
nextRendersAuthor: boolean
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
interface GroupMessagesProps {
|
|
300
|
-
ms: MessageResource[]
|
|
301
|
-
inReplyScreen?: boolean
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
|
|
305
|
-
let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
|
|
306
|
-
let encounteredOneOfMyMessages = false
|
|
307
|
-
|
|
308
|
-
ms.forEach((message, i) => {
|
|
309
|
-
const prevMessage = ms[i + 1]
|
|
310
|
-
const nextMessage = ms[i - 1]
|
|
311
|
-
const date = dayjs(message.createdAt).format('YYYY-MM-DD')
|
|
312
|
-
|
|
313
|
-
const prevMessageIsDateSeparator =
|
|
314
|
-
prevMessage && date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')
|
|
315
|
-
|
|
316
|
-
if (isSystemMessage(message)) {
|
|
317
|
-
message.myLatestInConversation = false
|
|
318
|
-
message.lastInGroup = true
|
|
319
|
-
message.renderAuthor = false
|
|
320
|
-
message.nextRendersAuthor = nextMessage?.renderAuthor
|
|
321
|
-
message.isReplyShadowMessage = false
|
|
322
|
-
message.nextIsReplyShadowMessage = false
|
|
323
|
-
message.threadPosition = null
|
|
324
|
-
enrichedMessages.push(message)
|
|
325
|
-
if (prevMessageIsDateSeparator) {
|
|
326
|
-
enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
|
|
327
|
-
}
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const inThread = message.replyRootId !== null
|
|
332
|
-
const nextMessageInThread = nextMessage?.replyRootId !== null
|
|
333
|
-
const threadRoot = message.replyRootId === message.id
|
|
334
|
-
const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id
|
|
335
|
-
const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId
|
|
336
|
-
const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId
|
|
337
|
-
const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id
|
|
338
|
-
const nextMessageDifferentAuthor = message.author?.id !== nextMessage?.author?.id
|
|
339
|
-
const prevMessageMoreThan5Minutes =
|
|
340
|
-
prevMessage &&
|
|
341
|
-
new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() > 60000 * 5
|
|
342
|
-
const nextMessageMoreThan5Minutes =
|
|
343
|
-
nextMessage &&
|
|
344
|
-
new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5
|
|
345
|
-
const nextMessageIsDateSeparator =
|
|
346
|
-
nextMessage && date !== dayjs(nextMessage.createdAt).format('YYYY-MM-DD')
|
|
347
|
-
const insertReplyShadowMessage =
|
|
348
|
-
message.replyRootId &&
|
|
349
|
-
!threadRoot &&
|
|
350
|
-
(prevMessageDifferentThread || prevMessageIsDateSeparator)
|
|
351
|
-
const lastInGroup =
|
|
352
|
-
!nextMessage ||
|
|
353
|
-
nextMessageDifferentAuthor ||
|
|
354
|
-
nextMessageMoreThan5Minutes ||
|
|
355
|
-
nextMessageDifferentThread ||
|
|
356
|
-
nextMessageIsDateSeparator
|
|
357
|
-
const renderAuthor =
|
|
358
|
-
!message.mine &&
|
|
359
|
-
(!prevMessage ||
|
|
360
|
-
prevMessageDifferentAuthor ||
|
|
361
|
-
prevMessageMoreThan5Minutes ||
|
|
362
|
-
prevMessageDifferentThread ||
|
|
363
|
-
prevMessageIsDateSeparator)
|
|
364
|
-
const nextIsReplyShadowMessage =
|
|
365
|
-
nextMessageInThread &&
|
|
366
|
-
!nextMessageThreadRoot &&
|
|
367
|
-
(nextMessageDifferentThread || nextMessageIsDateSeparator)
|
|
368
|
-
|
|
369
|
-
if (message.mine && !encounteredOneOfMyMessages) {
|
|
370
|
-
encounteredOneOfMyMessages = true
|
|
371
|
-
message.myLatestInConversation = true
|
|
372
|
-
} else {
|
|
373
|
-
message.myLatestInConversation = false
|
|
374
|
-
}
|
|
375
|
-
message.lastInGroup = lastInGroup
|
|
376
|
-
message.renderAuthor = renderAuthor
|
|
377
|
-
message.threadPosition = null
|
|
378
|
-
message.nextRendersAuthor = nextMessage?.renderAuthor
|
|
379
|
-
message.isReplyShadowMessage = false
|
|
380
|
-
message.nextIsReplyShadowMessage = nextIsReplyShadowMessage
|
|
381
|
-
|
|
382
|
-
if (!inReplyScreen && inThread) {
|
|
383
|
-
message.prevIsMyReply = prevMessage?.mine
|
|
384
|
-
message.nextIsMyReply = nextMessage?.mine
|
|
385
|
-
|
|
386
|
-
const firstInThread = threadRoot
|
|
387
|
-
const lastInThread = nextMessageDifferentThread || nextMessageIsDateSeparator
|
|
388
|
-
|
|
389
|
-
if (firstInThread && lastInThread)
|
|
390
|
-
message.threadPosition = null // ensures we don't render a connector for root replies that aren't immediately followed up a reply
|
|
391
|
-
else if (firstInThread) message.threadPosition = 'first'
|
|
392
|
-
else if (lastInThread) message.threadPosition = 'last'
|
|
393
|
-
else message.threadPosition = 'center'
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
enrichedMessages.push(message)
|
|
397
|
-
|
|
398
|
-
if (insertReplyShadowMessage) {
|
|
399
|
-
enrichedMessages.push({
|
|
400
|
-
type: 'ReplyShadowMessage',
|
|
401
|
-
id: `${message.id}-${message.replyRootId}`,
|
|
402
|
-
messageId: message.replyRootId!,
|
|
403
|
-
isReplyShadowMessage: true,
|
|
404
|
-
nextRendersAuthor: message?.renderAuthor,
|
|
405
|
-
})
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (!prevMessage || date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')) {
|
|
409
|
-
enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
|
|
410
|
-
}
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
return enrichedMessages
|
|
414
|
-
}
|
|
415
387
|
interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
416
388
|
conversation_id: number
|
|
417
389
|
badge?: ConversationBadgeResource
|
|
@@ -506,6 +478,10 @@ const useStyles = () => {
|
|
|
506
478
|
// Just whitespace to provide space where the typing indicator can be
|
|
507
479
|
height: 16,
|
|
508
480
|
},
|
|
481
|
+
loadingFooter: {
|
|
482
|
+
paddingVertical: 12,
|
|
483
|
+
alignItems: 'center',
|
|
484
|
+
},
|
|
509
485
|
})
|
|
510
486
|
}
|
|
511
487
|
|