@planningcenter/chat-react-native 4.2.1-qa-staging.0 → 4.2.1-qa-staging.2
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/anchor_not_found_boundary.d.ts +18 -0
- package/build/components/conversation/anchor_not_found_boundary.d.ts.map +1 -0
- package/build/components/conversation/anchor_not_found_boundary.js +20 -0
- package/build/components/conversation/anchor_not_found_boundary.js.map +1 -0
- package/build/components/conversation/message_list.d.ts +3 -2
- package/build/components/conversation/message_list.d.ts.map +1 -1
- package/build/components/conversation/message_list.js +149 -66
- package/build/components/conversation/message_list.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts +8 -2
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +77 -45
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_fetch_newer_on_foreground.d.ts +8 -0
- package/build/hooks/use_fetch_newer_on_foreground.d.ts.map +1 -0
- package/build/hooks/use_fetch_newer_on_foreground.js +41 -0
- package/build/hooks/use_fetch_newer_on_foreground.js.map +1 -0
- package/build/hooks/use_mark_latest_message_read.d.ts +2 -1
- package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
- package/build/hooks/use_mark_latest_message_read.js +4 -3
- package/build/hooks/use_mark_latest_message_read.js.map +1 -1
- package/build/screens/conversation_screen.d.ts +1 -0
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +119 -48
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/utils/generate_placeholder_ulid.d.ts +0 -6
- package/build/utils/generate_placeholder_ulid.d.ts.map +1 -1
- package/build/utils/generate_placeholder_ulid.js +2 -2
- package/build/utils/generate_placeholder_ulid.js.map +1 -1
- package/build/utils/request/get_messages.d.ts +18 -6
- package/build/utils/request/get_messages.d.ts.map +1 -1
- package/build/utils/request/get_messages.js +10 -12
- package/build/utils/request/get_messages.js.map +1 -1
- package/build/utils/ulid.d.ts +3 -0
- package/build/utils/ulid.d.ts.map +1 -0
- package/build/utils/ulid.js +7 -0
- package/build/utils/ulid.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/utils/request/get_messages.test.ts +24 -0
- package/src/__tests__/utils/ulid.test.ts +15 -0
- package/src/components/conversation/__tests__/anchor_not_found_boundary.test.tsx +51 -0
- package/src/components/conversation/__tests__/message_list.test.tsx +42 -0
- package/src/components/conversation/anchor_not_found_boundary.tsx +21 -0
- package/src/components/conversation/message_list.tsx +50 -3
- package/src/hooks/use_conversation_messages.ts +43 -11
- package/src/hooks/use_fetch_newer_on_foreground.ts +25 -0
- package/src/hooks/use_mark_latest_message_read.ts +4 -3
- package/src/screens/conversation_screen.tsx +92 -21
- package/src/utils/generate_placeholder_ulid.ts +2 -2
- package/src/utils/request/get_messages.ts +12 -14
- package/src/utils/ulid.ts +11 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { date as formatDate } from '@planningcenter/datetime-fmt'
|
|
2
|
-
import
|
|
2
|
+
import { noop } from 'lodash'
|
|
3
|
+
import React, { RefObject, useRef } from 'react'
|
|
3
4
|
import { FlatList, FlatListProps, StyleSheet, View } from 'react-native'
|
|
4
5
|
import { useTheme } from '../../hooks'
|
|
5
6
|
import { ConversationResource } from '../../types/resources/conversation'
|
|
@@ -21,6 +22,9 @@ type ScrollHandlers = Pick<
|
|
|
21
22
|
| 'onContentSizeChange'
|
|
22
23
|
| 'onScrollEndDrag'
|
|
23
24
|
| 'onMomentumScrollEnd'
|
|
25
|
+
| 'onScrollBeginDrag'
|
|
26
|
+
| 'onStartReached'
|
|
27
|
+
| 'onStartReachedThreshold'
|
|
24
28
|
| 'maintainVisibleContentPosition'
|
|
25
29
|
>
|
|
26
30
|
|
|
@@ -30,6 +34,7 @@ interface MessageListProps extends ScrollHandlers {
|
|
|
30
34
|
conversationId: number
|
|
31
35
|
messages: MessageResource[]
|
|
32
36
|
replyRootId?: string | null
|
|
37
|
+
anchorMessageId?: string | null
|
|
33
38
|
hasNextPage: boolean
|
|
34
39
|
onEndReached: () => void
|
|
35
40
|
}
|
|
@@ -43,6 +48,7 @@ export function MessageList({
|
|
|
43
48
|
conversationId,
|
|
44
49
|
messages,
|
|
45
50
|
replyRootId,
|
|
51
|
+
anchorMessageId,
|
|
46
52
|
hasNextPage,
|
|
47
53
|
onEndReached,
|
|
48
54
|
onScroll,
|
|
@@ -50,6 +56,9 @@ export function MessageList({
|
|
|
50
56
|
onContentSizeChange,
|
|
51
57
|
onScrollEndDrag,
|
|
52
58
|
onMomentumScrollEnd,
|
|
59
|
+
onScrollBeginDrag,
|
|
60
|
+
onStartReached,
|
|
61
|
+
onStartReachedThreshold,
|
|
53
62
|
maintainVisibleContentPosition,
|
|
54
63
|
}: MessageListProps) {
|
|
55
64
|
const styles = useStyles()
|
|
@@ -58,6 +67,40 @@ export function MessageList({
|
|
|
58
67
|
conversation.memberAbility?.canDeleteNonAuthoredMessages ?? false
|
|
59
68
|
const latestRead = conversation.latestReadMessageSortKey
|
|
60
69
|
const messagesWithSeparators = groupMessages({ ms: messages, inReplyScreen })
|
|
70
|
+
const anchorIndex = anchorMessageId
|
|
71
|
+
? messagesWithSeparators.findIndex(item => item.id === anchorMessageId)
|
|
72
|
+
: -1
|
|
73
|
+
const layoutHeight = useRef(0)
|
|
74
|
+
const contentHeight = useRef(0)
|
|
75
|
+
const userHasScrolled = useRef(false)
|
|
76
|
+
|
|
77
|
+
const pinAnchorToBottomEdge = () => {
|
|
78
|
+
if (anchorIndex < 0 || userHasScrolled.current) return
|
|
79
|
+
const scrollable = layoutHeight.current > 0 && contentHeight.current > layoutHeight.current
|
|
80
|
+
if (!scrollable) return
|
|
81
|
+
|
|
82
|
+
listRef.current?.scrollToIndex({ index: anchorIndex, viewPosition: 0, animated: false })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleLayout: FlatListProps<EnrichedMessage>['onLayout'] = event => {
|
|
86
|
+
onLayout?.(event)
|
|
87
|
+
layoutHeight.current = event.nativeEvent.layout.height
|
|
88
|
+
pinAnchorToBottomEdge()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const handleContentSizeChange: FlatListProps<EnrichedMessage>['onContentSizeChange'] = (
|
|
92
|
+
width,
|
|
93
|
+
height
|
|
94
|
+
) => {
|
|
95
|
+
onContentSizeChange?.(width, height)
|
|
96
|
+
contentHeight.current = height
|
|
97
|
+
pinAnchorToBottomEdge()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleScrollBeginDrag: FlatListProps<EnrichedMessage>['onScrollBeginDrag'] = event => {
|
|
101
|
+
userHasScrolled.current = true
|
|
102
|
+
onScrollBeginDrag?.(event)
|
|
103
|
+
}
|
|
61
104
|
|
|
62
105
|
return (
|
|
63
106
|
<FlatList
|
|
@@ -67,8 +110,9 @@ export function MessageList({
|
|
|
67
110
|
data={messagesWithSeparators}
|
|
68
111
|
keyExtractor={item => item.id}
|
|
69
112
|
onScroll={onScroll}
|
|
70
|
-
onLayout={
|
|
71
|
-
onContentSizeChange={
|
|
113
|
+
onLayout={handleLayout}
|
|
114
|
+
onContentSizeChange={handleContentSizeChange}
|
|
115
|
+
onScrollToIndexFailed={noop}
|
|
72
116
|
onScrollEndDrag={onScrollEndDrag}
|
|
73
117
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
74
118
|
scrollEventThrottle={10}
|
|
@@ -108,6 +152,9 @@ export function MessageList({
|
|
|
108
152
|
)
|
|
109
153
|
}}
|
|
110
154
|
onEndReached={onEndReached}
|
|
155
|
+
onScrollBeginDrag={handleScrollBeginDrag}
|
|
156
|
+
onStartReached={onStartReached}
|
|
157
|
+
onStartReachedThreshold={onStartReachedThreshold}
|
|
111
158
|
maintainVisibleContentPosition={maintainVisibleContentPosition}
|
|
112
159
|
ListHeaderComponent={<View style={styles.listHeader} />}
|
|
113
160
|
/>
|
|
@@ -2,21 +2,43 @@ import { useMemo } from 'react'
|
|
|
2
2
|
import { MessageResource } from '../types'
|
|
3
3
|
import { replaceEqualById } from '../utils/replace_equal_by_id'
|
|
4
4
|
import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
5
|
+
import { getNewerPageParam } from './paginator_meta'
|
|
5
6
|
import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
|
|
6
7
|
|
|
8
|
+
type Options = {
|
|
9
|
+
conversation_id: number
|
|
10
|
+
reply_root_id?: string | null
|
|
11
|
+
anchor?: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export const useConversationMessages = (
|
|
8
|
-
{ conversation_id, reply_root_id }:
|
|
15
|
+
{ conversation_id, reply_root_id, anchor }: Options,
|
|
9
16
|
opts?: SuspensePaginatorOptions
|
|
10
17
|
) => {
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const {
|
|
19
|
+
data,
|
|
20
|
+
refetch,
|
|
21
|
+
isRefetching,
|
|
22
|
+
fetchNextPage,
|
|
23
|
+
hasNextPage,
|
|
24
|
+
fetchPreviousPage,
|
|
25
|
+
hasPreviousPage,
|
|
26
|
+
isFetchingPreviousPage,
|
|
27
|
+
} = useSuspensePaginator<MessageResource>(
|
|
28
|
+
getMessagesRequestArgs({ conversation_id, reply_root_id, anchor }),
|
|
29
|
+
{
|
|
30
|
+
structuralSharing: replaceEqualById,
|
|
31
|
+
...(anchor
|
|
32
|
+
? {
|
|
33
|
+
getPreviousPageParam: (page, _pages, pageParam) => getNewerPageParam(page, pageParam),
|
|
34
|
+
gcTime: 0,
|
|
35
|
+
staleTime: Infinity,
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
...opts,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id, anchor })
|
|
20
42
|
const messages = useMemo(
|
|
21
43
|
() =>
|
|
22
44
|
data
|
|
@@ -29,5 +51,15 @@ export const useConversationMessages = (
|
|
|
29
51
|
[data]
|
|
30
52
|
)
|
|
31
53
|
|
|
32
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
messages,
|
|
56
|
+
refetch,
|
|
57
|
+
isRefetching,
|
|
58
|
+
fetchNextPage,
|
|
59
|
+
hasNextPage,
|
|
60
|
+
fetchPreviousPage,
|
|
61
|
+
hasPreviousPage,
|
|
62
|
+
isFetchingPreviousPage,
|
|
63
|
+
queryKey,
|
|
64
|
+
}
|
|
33
65
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { useAppState } from './use_app_state'
|
|
3
|
+
|
|
4
|
+
type Options = {
|
|
5
|
+
fetchNewer: (newestMessageId: string) => void
|
|
6
|
+
newestMessageId?: string
|
|
7
|
+
enabled: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useFetchNewerOnForeground = ({ fetchNewer, newestMessageId, enabled }: Options) => {
|
|
11
|
+
const appState = useAppState()
|
|
12
|
+
const wasInBackground = useRef(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (appState !== 'active') {
|
|
16
|
+
wasInBackground.current = true
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
const returnedToForeground = wasInBackground.current
|
|
20
|
+
wasInBackground.current = false
|
|
21
|
+
if (!returnedToForeground || !enabled || !newestMessageId) return
|
|
22
|
+
|
|
23
|
+
fetchNewer(newestMessageId)
|
|
24
|
+
}, [appState, enabled, fetchNewer, newestMessageId])
|
|
25
|
+
}
|
|
@@ -7,9 +7,10 @@ import { useConversationsMarkRead } from './use_conversations_actions'
|
|
|
7
7
|
interface Props {
|
|
8
8
|
conversation: ConversationResource
|
|
9
9
|
messages: MessageResource[]
|
|
10
|
+
enabled?: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function useMarkLatestMessageRead({ conversation }: Props) {
|
|
13
|
+
export function useMarkLatestMessageRead({ conversation, enabled = true }: Props) {
|
|
13
14
|
const firedOnce = useRef<boolean>(false)
|
|
14
15
|
const { markRead } = useConversationsMarkRead({ conversation })
|
|
15
16
|
const debouncedMarkRead = useMemo(
|
|
@@ -24,10 +25,10 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
|
|
|
24
25
|
const isActive = appState === 'active'
|
|
25
26
|
|
|
26
27
|
useEffect(() => {
|
|
27
|
-
if (!isActive || !shouldMarkRead) return
|
|
28
|
+
if (!enabled || !isActive || !shouldMarkRead) return
|
|
28
29
|
|
|
29
30
|
firedOnce.current = true
|
|
30
31
|
|
|
31
32
|
debouncedMarkRead(true)
|
|
32
|
-
}, [debouncedMarkRead, isActive, shouldMarkRead, unreadReaction])
|
|
33
|
+
}, [debouncedMarkRead, enabled, isActive, shouldMarkRead, unreadReaction])
|
|
33
34
|
}
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
useTheme as useNavigationTheme,
|
|
8
8
|
useRoute,
|
|
9
9
|
} from '@react-navigation/native'
|
|
10
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
10
|
+
import React, { startTransition, useCallback, useEffect, useRef, useState } from 'react'
|
|
11
11
|
import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
12
12
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
13
13
|
import { Badge, Icon } from '../components'
|
|
14
|
+
import { AnchorNotFoundBoundary } from '../components/conversation/anchor_not_found_boundary'
|
|
14
15
|
import { ConversationStartBanner } from '../components/conversation/conversation_start_banner'
|
|
15
16
|
import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
|
|
16
17
|
import { MessageForm } from '../components/conversation/message_form'
|
|
@@ -31,6 +32,7 @@ import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events
|
|
|
31
32
|
import { useConversationMessages } from '../hooks/use_conversation_messages'
|
|
32
33
|
import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
|
|
33
34
|
import { useFetchNewerMessages } from '../hooks/use_fetch_newer_messages'
|
|
35
|
+
import { useFetchNewerOnForeground } from '../hooks/use_fetch_newer_on_foreground'
|
|
34
36
|
import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
|
|
35
37
|
import {
|
|
36
38
|
analyticsEvents,
|
|
@@ -43,6 +45,7 @@ import { ConversationResource } from '../types/resources/conversation'
|
|
|
43
45
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge'
|
|
44
46
|
import { MessageResource } from '../types/resources/message'
|
|
45
47
|
import { gatedMessageCache } from '../utils/gated_message_cache'
|
|
48
|
+
import { decodeUlidTime } from '../utils/ulid'
|
|
46
49
|
|
|
47
50
|
export type ConversationRouteProps = {
|
|
48
51
|
conversation_id: number
|
|
@@ -51,6 +54,7 @@ export type ConversationRouteProps = {
|
|
|
51
54
|
chat_group_graph_id?: string
|
|
52
55
|
clear_input?: boolean
|
|
53
56
|
editing_message_id?: number | null
|
|
57
|
+
initial_message_id?: string | null
|
|
54
58
|
title?: string
|
|
55
59
|
subtitle?: string
|
|
56
60
|
badge?: ConversationBadgeResource
|
|
@@ -61,9 +65,16 @@ export type ConversationRouteProps = {
|
|
|
61
65
|
export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
|
|
62
66
|
|
|
63
67
|
export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
64
|
-
const { conversation_id, reply_root_id } = route.params
|
|
68
|
+
const { conversation_id, reply_root_id, initial_message_id } = route.params
|
|
65
69
|
|
|
66
70
|
const { data: conversation } = useConversation({ conversation_id })
|
|
71
|
+
const [anchor, setAnchor] = useState(() =>
|
|
72
|
+
isBeforeLatestMessage(initial_message_id, conversation?.lastMessageCreatedAt)
|
|
73
|
+
? initial_message_id
|
|
74
|
+
: null
|
|
75
|
+
)
|
|
76
|
+
const exitAnchoredMode = useCallback(() => startTransition(() => setAnchor(null)), [])
|
|
77
|
+
const dropAnchor = useCallback(() => setAnchor(null), [])
|
|
67
78
|
|
|
68
79
|
usePublishProductAnalyticsEvent(analyticsEvents.conversation_show_opened, {
|
|
69
80
|
reply_root_id,
|
|
@@ -75,28 +86,59 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
75
86
|
conversationId={conversation_id}
|
|
76
87
|
currentPageReplyRootId={reply_root_id ?? null}
|
|
77
88
|
>
|
|
78
|
-
|
|
89
|
+
{anchor ? (
|
|
90
|
+
<AnchorNotFoundBoundary onAnchorNotFound={dropAnchor}>
|
|
91
|
+
<ConversationScreenContent
|
|
92
|
+
route={route}
|
|
93
|
+
anchor={anchor}
|
|
94
|
+
onExitAnchoredMode={exitAnchoredMode}
|
|
95
|
+
/>
|
|
96
|
+
</AnchorNotFoundBoundary>
|
|
97
|
+
) : (
|
|
98
|
+
<ConversationScreenContent
|
|
99
|
+
route={route}
|
|
100
|
+
anchor={null}
|
|
101
|
+
onExitAnchoredMode={exitAnchoredMode}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
79
104
|
</ConversationContextProvider>
|
|
80
105
|
)
|
|
81
106
|
}
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
interface ConversationScreenContentProps extends ConversationScreenProps {
|
|
109
|
+
anchor: string | null | undefined
|
|
110
|
+
onExitAnchoredMode: () => void
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ConversationScreenContent({
|
|
114
|
+
route,
|
|
115
|
+
anchor,
|
|
116
|
+
onExitAnchoredMode,
|
|
117
|
+
}: ConversationScreenContentProps) {
|
|
84
118
|
const styles = useStyles()
|
|
85
119
|
const navigation = useNavigation()
|
|
86
120
|
const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
|
|
87
121
|
route.params
|
|
88
122
|
const { data: conversation } = useConversation(route.params)
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
const anchored = !!anchor
|
|
124
|
+
const {
|
|
125
|
+
messages,
|
|
126
|
+
fetchNextPage,
|
|
127
|
+
hasNextPage,
|
|
128
|
+
fetchPreviousPage,
|
|
129
|
+
hasPreviousPage,
|
|
130
|
+
isFetchingPreviousPage,
|
|
131
|
+
queryKey,
|
|
132
|
+
} = useConversationMessages({ conversation_id, reply_root_id, anchor })
|
|
133
|
+
const behindLatest = anchored && hasPreviousPage
|
|
134
|
+
const caughtUp = !hasPreviousPage
|
|
93
135
|
const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
|
|
94
136
|
const readingHistory = showJumpToBottomButton
|
|
95
137
|
const skippedNewMessages = useRef(false)
|
|
96
138
|
const messageCaches = [
|
|
97
139
|
gatedMessageCache({
|
|
98
140
|
queryKey,
|
|
99
|
-
acceptNewMessages: !readingHistory,
|
|
141
|
+
acceptNewMessages: caughtUp && !readingHistory,
|
|
100
142
|
onSkippedNewMessage: () => {
|
|
101
143
|
skippedNewMessages.current = true
|
|
102
144
|
},
|
|
@@ -107,8 +149,9 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
107
149
|
useConversationMessagesJoltEvents({ conversationId: conversation_id, messageCaches })
|
|
108
150
|
const { markHandledByMessageForm } = useVideoModerationJoltEvents()
|
|
109
151
|
useEnsureConversationsRouteExists()
|
|
110
|
-
useMarkLatestMessageRead({ conversation, messages })
|
|
152
|
+
useMarkLatestMessageRead({ conversation, messages, enabled: !behindLatest })
|
|
111
153
|
const fetchNewer = useFetchNewerMessages({ conversation_id, reply_root_id, queryKey })
|
|
154
|
+
useFetchNewerOnForeground({ fetchNewer, newestMessageId, enabled: anchored && caughtUp })
|
|
112
155
|
const noMessages = messages.length === 0
|
|
113
156
|
|
|
114
157
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
@@ -123,11 +166,8 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
123
166
|
const muted = conversation.conversationMembership?.muted ?? conversation.muted
|
|
124
167
|
|
|
125
168
|
const listRef = useRef<FlatList>(null)
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const offsetY = event.nativeEvent.contentOffset.y
|
|
129
|
-
setShowJumpToBottomButton(offsetY > 200)
|
|
130
|
-
}
|
|
169
|
+
const [hasDragged, setHasDragged] = useState(false)
|
|
170
|
+
const holdListPosition = behindLatest || readingHistory
|
|
131
171
|
|
|
132
172
|
const { restOffset, onLayout, onContentSizeChange, onMomentumScrollEnd, onScrollEndDrag } =
|
|
133
173
|
useRestAwayFromDismissEdge({
|
|
@@ -141,17 +181,34 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
141
181
|
})
|
|
142
182
|
}, [restOffset])
|
|
143
183
|
|
|
184
|
+
const exitAnchoredMode = useCallback(() => {
|
|
185
|
+
onExitAnchoredMode()
|
|
186
|
+
handleReturnToBottom()
|
|
187
|
+
}, [handleReturnToBottom, onExitAnchoredMode])
|
|
144
188
|
const jumpToLatest = useCallback(() => {
|
|
145
189
|
setShowJumpToBottomButton(false)
|
|
146
|
-
|
|
147
|
-
|
|
190
|
+
if (behindLatest) exitAnchoredMode()
|
|
191
|
+
else handleReturnToBottom()
|
|
192
|
+
}, [behindLatest, exitAnchoredMode, handleReturnToBottom])
|
|
193
|
+
|
|
194
|
+
const trackScroll = (event: any) => {
|
|
195
|
+
const offsetY = event.nativeEvent.contentOffset.y
|
|
196
|
+
setShowJumpToBottomButton(offsetY > 200)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const loadNewerMessages = () => {
|
|
200
|
+
if (!hasPreviousPage || isFetchingPreviousPage) return
|
|
201
|
+
|
|
202
|
+
setHasDragged(false)
|
|
203
|
+
fetchPreviousPage()
|
|
204
|
+
}
|
|
148
205
|
|
|
149
206
|
useEffect(() => {
|
|
150
|
-
if (readingHistory || !newestMessageId || !skippedNewMessages.current) return
|
|
207
|
+
if (readingHistory || !caughtUp || !newestMessageId || !skippedNewMessages.current) return
|
|
151
208
|
|
|
152
209
|
skippedNewMessages.current = false
|
|
153
210
|
fetchNewer(newestMessageId)
|
|
154
|
-
}, [fetchNewer, newestMessageId, readingHistory])
|
|
211
|
+
}, [caughtUp, fetchNewer, newestMessageId, readingHistory])
|
|
155
212
|
|
|
156
213
|
useEffect(() => {
|
|
157
214
|
if (reply_root_id) {
|
|
@@ -201,6 +258,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
201
258
|
conversationId={conversation_id}
|
|
202
259
|
messages={messages}
|
|
203
260
|
replyRootId={reply_root_id}
|
|
261
|
+
anchorMessageId={anchor}
|
|
204
262
|
hasNextPage={hasNextPage}
|
|
205
263
|
onEndReached={() => fetchNextPage()}
|
|
206
264
|
onScroll={trackScroll}
|
|
@@ -208,10 +266,18 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
208
266
|
onContentSizeChange={onContentSizeChange}
|
|
209
267
|
onScrollEndDrag={onScrollEndDrag}
|
|
210
268
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
211
|
-
|
|
269
|
+
onScrollBeginDrag={anchored ? () => setHasDragged(true) : undefined}
|
|
270
|
+
onStartReached={anchored && hasDragged ? loadNewerMessages : undefined}
|
|
271
|
+
onStartReachedThreshold={anchored ? 0.5 : undefined}
|
|
272
|
+
maintainVisibleContentPosition={
|
|
273
|
+
holdListPosition ? { minIndexForVisible: 0 } : undefined
|
|
274
|
+
}
|
|
212
275
|
/>
|
|
213
276
|
)}
|
|
214
|
-
<JumpToBottomButton
|
|
277
|
+
<JumpToBottomButton
|
|
278
|
+
onPress={jumpToLatest}
|
|
279
|
+
visible={behindLatest || showJumpToBottomButton}
|
|
280
|
+
/>
|
|
215
281
|
{!noMessages && <TypingIndicator />}
|
|
216
282
|
{showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
|
|
217
283
|
<ConversationBottomBar
|
|
@@ -279,6 +345,11 @@ function ConversationBottomBar({
|
|
|
279
345
|
)
|
|
280
346
|
}
|
|
281
347
|
|
|
348
|
+
const isBeforeLatestMessage = (anchor?: string | null, latestMessageCreatedAt?: string) =>
|
|
349
|
+
!!anchor &&
|
|
350
|
+
!!latestMessageCreatedAt &&
|
|
351
|
+
decodeUlidTime(anchor) < Date.parse(latestMessageCreatedAt)
|
|
352
|
+
|
|
282
353
|
interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
283
354
|
conversation_id: number
|
|
284
355
|
badge?: ConversationBadgeResource
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ULID_CHARS } from './ulid'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The actual `ulid` library depends on a secure random number generator, which looks like it
|
|
3
5
|
* may require a native dependency to polyfill in React Native.
|
|
@@ -5,8 +7,6 @@
|
|
|
5
7
|
* implementation that doesn't require any native dependencies.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
const ULID_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' // Crockford's Base32
|
|
9
|
-
|
|
10
10
|
function encodeBase32(value: number, length: number) {
|
|
11
11
|
let result = ''
|
|
12
12
|
for (let i = 0; i < length; i++) {
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { getRequestQueryKey } from '../../hooks/use_suspense_api'
|
|
2
2
|
import { getMessageFields, getMessagesInclude } from './messages_data_options'
|
|
3
3
|
|
|
4
|
+
type MessagesRequestOptions = {
|
|
5
|
+
conversation_id: number
|
|
6
|
+
reply_root_id?: string | null
|
|
7
|
+
anchor?: string | null
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export const getMessagesRequestArgs = ({
|
|
5
11
|
conversation_id,
|
|
6
12
|
reply_root_id,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
reply_root_id?: string | null
|
|
10
|
-
}) => {
|
|
13
|
+
anchor,
|
|
14
|
+
}: MessagesRequestOptions) => {
|
|
11
15
|
const normalizedReplyRootId = reply_root_id ?? null
|
|
12
16
|
const url = normalizedReplyRootId
|
|
13
17
|
? `/me/conversations/${conversation_id}/messages/${normalizedReplyRootId}/replies`
|
|
14
18
|
: `/me/conversations/${conversation_id}/messages`
|
|
19
|
+
const anchored = anchor ? { where: { id_around: anchor } } : {}
|
|
15
20
|
|
|
16
21
|
return {
|
|
17
22
|
url,
|
|
@@ -19,18 +24,11 @@ export const getMessagesRequestArgs = ({
|
|
|
19
24
|
perPage: 25,
|
|
20
25
|
fields: getMessageFields,
|
|
21
26
|
include: getMessagesInclude,
|
|
27
|
+
...anchored,
|
|
22
28
|
},
|
|
23
29
|
reply_root_id: normalizedReplyRootId,
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
export const getMessagesQueryKey = (
|
|
28
|
-
|
|
29
|
-
reply_root_id,
|
|
30
|
-
}: {
|
|
31
|
-
conversation_id: number
|
|
32
|
-
reply_root_id?: string | null
|
|
33
|
-
}) => {
|
|
34
|
-
const requestArgs = getMessagesRequestArgs({ conversation_id, reply_root_id })
|
|
35
|
-
return getRequestQueryKey(requestArgs)
|
|
36
|
-
}
|
|
33
|
+
export const getMessagesQueryKey = (options: MessagesRequestOptions) =>
|
|
34
|
+
getRequestQueryKey(getMessagesRequestArgs(options))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const ULID_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' // Crockford's Base32
|
|
2
|
+
|
|
3
|
+
const TIME_LENGTH = 10
|
|
4
|
+
|
|
5
|
+
export function decodeUlidTime(ulid: string): number {
|
|
6
|
+
return ulid
|
|
7
|
+
.slice(0, TIME_LENGTH)
|
|
8
|
+
.toUpperCase()
|
|
9
|
+
.split('')
|
|
10
|
+
.reduce((time, char) => time * 32 + ULID_CHARS.indexOf(char), 0)
|
|
11
|
+
}
|