@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,64 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { ApiCollection, MessageResource } from '../../types'
|
|
2
|
-
import {
|
|
3
|
-
anchoredSeedPageParams,
|
|
4
|
-
newerPageParam,
|
|
5
|
-
olderPageParam,
|
|
6
|
-
sortAndFilterMessages,
|
|
7
|
-
} from '../conversation_messages'
|
|
8
|
-
|
|
9
|
-
const message = (id: string, overrides: Partial<MessageResource> = {}): MessageResource =>
|
|
10
|
-
({
|
|
11
|
-
id,
|
|
12
|
-
type: 'Message',
|
|
13
|
-
text: `msg ${id}`,
|
|
14
|
-
attachments: [],
|
|
15
|
-
deletedAt: null,
|
|
16
|
-
replyRootId: null,
|
|
17
|
-
...overrides,
|
|
18
|
-
}) as MessageResource
|
|
19
|
-
|
|
20
|
-
const page = (
|
|
21
|
-
data: MessageResource[],
|
|
22
|
-
next?: { idLt?: string; idGt?: string }
|
|
23
|
-
): ApiCollection<MessageResource> => ({
|
|
24
|
-
data,
|
|
25
|
-
links: {},
|
|
26
|
-
meta: { count: data.length, totalCount: data.length, next },
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
describe('anchoredSeedPageParams', () => {
|
|
30
|
-
it('returns one ascending after-page and one descending before-page', () => {
|
|
31
|
-
expect(anchoredSeedPageParams('01ABC')).toEqual([
|
|
32
|
-
{ where: { id_gte: '01ABC' }, order: 'asc' },
|
|
33
|
-
{ where: { id_lt: '01ABC' }, order: 'desc' },
|
|
34
|
-
])
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
describe('olderPageParam', () => {
|
|
39
|
-
it('returns id_lt cursor when meta.next.idLt is present', () => {
|
|
40
|
-
expect(olderPageParam(page([], { idLt: '01XYZ' }))).toEqual({
|
|
41
|
-
where: { id_lt: '01XYZ' },
|
|
42
|
-
order: 'desc',
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('returns undefined when meta.next.idLt is missing', () => {
|
|
47
|
-
expect(olderPageParam(page([], {}))).toBeUndefined()
|
|
48
|
-
expect(olderPageParam(page([], { idGt: '01XYZ' }))).toBeUndefined()
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe('newerPageParam', () => {
|
|
53
|
-
it('returns id_gt cursor when meta.next.idGt is present', () => {
|
|
54
|
-
expect(newerPageParam(page([], { idGt: '01XYZ' }))).toEqual({
|
|
55
|
-
where: { id_gt: '01XYZ' },
|
|
56
|
-
order: 'asc',
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('returns undefined when meta.next.idGt is missing', () => {
|
|
61
|
-
expect(newerPageParam(page([], {}))).toBeUndefined()
|
|
62
|
-
expect(newerPageParam(page([], { idLt: '01XYZ' }))).toBeUndefined()
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
describe('sortAndFilterMessages', () => {
|
|
67
|
-
it('flattens pages and sorts descending by id (newest first)', () => {
|
|
68
|
-
const pages = [page([message('01B'), message('01A')]), page([message('01D'), message('01C')])]
|
|
69
|
-
|
|
70
|
-
expect(sortAndFilterMessages(pages).map(m => m.id)).toEqual(['01D', '01C', '01B', '01A'])
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('drops empty messages (no text, no attachments)', () => {
|
|
74
|
-
const pages = [page([message('01A'), message('01B', { text: '' })])]
|
|
75
|
-
|
|
76
|
-
expect(sortAndFilterMessages(pages).map(m => m.id)).toEqual(['01A'])
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('drops deleted messages outside reply threads', () => {
|
|
80
|
-
const pages = [page([message('01A'), message('01B', { deletedAt: '2026-01-01T00:00:00Z' })])]
|
|
81
|
-
|
|
82
|
-
expect(sortAndFilterMessages(pages).map(m => m.id)).toEqual(['01A'])
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('keeps deleted reply-thread messages so threads do not break', () => {
|
|
86
|
-
const pages = [
|
|
87
|
-
page([
|
|
88
|
-
message('01A'),
|
|
89
|
-
message('01B', { deletedAt: '2026-01-01T00:00:00Z', replyRootId: '01A' }),
|
|
90
|
-
]),
|
|
91
|
-
]
|
|
92
|
-
|
|
93
|
-
expect(sortAndFilterMessages(pages).map(m => m.id)).toEqual(['01B', '01A'])
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('keeps messages with attachments even when text is empty', () => {
|
|
97
|
-
const pages = [
|
|
98
|
-
page([
|
|
99
|
-
message('01A', { text: '', attachments: [{ id: '1' }] as MessageResource['attachments'] }),
|
|
100
|
-
]),
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
expect(sortAndFilterMessages(pages).map(m => m.id)).toEqual(['01A'])
|
|
104
|
-
})
|
|
105
|
-
})
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { makeHighestSeenTracker } from '../highest_seen_tracker'
|
|
2
|
-
|
|
3
|
-
describe('makeHighestSeenTracker', () => {
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
jest.useFakeTimers()
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
jest.useRealTimers()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('debounces and sends the highest sort key seen so far', () => {
|
|
13
|
-
const send = jest.fn()
|
|
14
|
-
const tracker = makeHighestSeenTracker(42, send, 2000)
|
|
15
|
-
|
|
16
|
-
tracker.onSeen('10')
|
|
17
|
-
tracker.onSeen('15')
|
|
18
|
-
tracker.onSeen('12')
|
|
19
|
-
|
|
20
|
-
expect(send).not.toHaveBeenCalled()
|
|
21
|
-
jest.advanceTimersByTime(2000)
|
|
22
|
-
|
|
23
|
-
expect(send).toHaveBeenCalledWith({ conversationId: 42, sortKey: '15' })
|
|
24
|
-
expect(send).toHaveBeenCalledTimes(1)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('ignores sort keys that are not strictly greater than the current highest', () => {
|
|
28
|
-
const send = jest.fn()
|
|
29
|
-
const tracker = makeHighestSeenTracker(1, send, 1000)
|
|
30
|
-
|
|
31
|
-
tracker.onSeen('20')
|
|
32
|
-
tracker.onSeen('20')
|
|
33
|
-
tracker.onSeen('19')
|
|
34
|
-
jest.advanceTimersByTime(1000)
|
|
35
|
-
|
|
36
|
-
expect(send).toHaveBeenCalledTimes(1)
|
|
37
|
-
expect(send).toHaveBeenCalledWith({ conversationId: 1, sortKey: '20' })
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('does not re-send the same sort key on subsequent flushes', () => {
|
|
41
|
-
const send = jest.fn()
|
|
42
|
-
const tracker = makeHighestSeenTracker(1, send, 1000)
|
|
43
|
-
|
|
44
|
-
tracker.onSeen('5')
|
|
45
|
-
jest.advanceTimersByTime(1000)
|
|
46
|
-
tracker.flushNow()
|
|
47
|
-
|
|
48
|
-
expect(send).toHaveBeenCalledTimes(1)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('flushNow fires immediately without waiting for the debounce', () => {
|
|
52
|
-
const send = jest.fn()
|
|
53
|
-
const tracker = makeHighestSeenTracker(1, send, 5000)
|
|
54
|
-
|
|
55
|
-
tracker.onSeen('3')
|
|
56
|
-
tracker.flushNow()
|
|
57
|
-
|
|
58
|
-
expect(send).toHaveBeenCalledWith({ conversationId: 1, sortKey: '3' })
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('cancel prevents a pending fire', () => {
|
|
62
|
-
const send = jest.fn()
|
|
63
|
-
const tracker = makeHighestSeenTracker(1, send, 1000)
|
|
64
|
-
|
|
65
|
-
tracker.onSeen('7')
|
|
66
|
-
tracker.cancel()
|
|
67
|
-
jest.advanceTimersByTime(1000)
|
|
68
|
-
|
|
69
|
-
expect(send).not.toHaveBeenCalled()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('orders fixed-width sort keys via lexicographic comparison', () => {
|
|
73
|
-
const send = jest.fn()
|
|
74
|
-
const tracker = makeHighestSeenTracker(1, send, 100)
|
|
75
|
-
|
|
76
|
-
tracker.onSeen('09')
|
|
77
|
-
tracker.onSeen('10')
|
|
78
|
-
jest.advanceTimersByTime(100)
|
|
79
|
-
|
|
80
|
-
expect(send).toHaveBeenCalledWith({ conversationId: 1, sortKey: '10' })
|
|
81
|
-
})
|
|
82
|
-
})
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
detectDividerExitTowardNewer,
|
|
3
|
-
reportViewableMessages,
|
|
4
|
-
type ViewabilityEvent,
|
|
5
|
-
} from '../message_viewability'
|
|
6
|
-
|
|
7
|
-
type Msg = { id: string; type: 'Message' }
|
|
8
|
-
|
|
9
|
-
const event = (overrides: Partial<ViewabilityEvent<Msg>> = {}): ViewabilityEvent<Msg> => ({
|
|
10
|
-
viewableItems: [],
|
|
11
|
-
changed: [],
|
|
12
|
-
userHasScrolled: true,
|
|
13
|
-
...overrides,
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe('reportViewableMessages', () => {
|
|
17
|
-
it('fires onMessageSeen for every viewable Message item by id', () => {
|
|
18
|
-
const onSeen = jest.fn()
|
|
19
|
-
const observer = reportViewableMessages<Msg>(onSeen)
|
|
20
|
-
|
|
21
|
-
observer(
|
|
22
|
-
event({
|
|
23
|
-
viewableItems: [
|
|
24
|
-
{ key: '10', isViewable: true, item: { id: '10', type: 'Message' } },
|
|
25
|
-
{ key: '11', isViewable: true, item: { id: '11', type: 'Message' } },
|
|
26
|
-
],
|
|
27
|
-
})
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
expect(onSeen).toHaveBeenCalledTimes(2)
|
|
31
|
-
expect(onSeen).toHaveBeenNthCalledWith(1, '10')
|
|
32
|
-
expect(onSeen).toHaveBeenNthCalledWith(2, '11')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('skips non-Message items (dividers, separators, shadows)', () => {
|
|
36
|
-
const onSeen = jest.fn()
|
|
37
|
-
const observer = reportViewableMessages<{ id?: string; type?: string }>(onSeen)
|
|
38
|
-
|
|
39
|
-
observer(
|
|
40
|
-
event({
|
|
41
|
-
viewableItems: [
|
|
42
|
-
{ key: 'divider', isViewable: true, item: { id: 'divider', type: 'UnreadDivider' } },
|
|
43
|
-
{
|
|
44
|
-
key: 'day-divider-05',
|
|
45
|
-
isViewable: true,
|
|
46
|
-
item: { id: 'day-divider-05', type: 'DateSeparator' },
|
|
47
|
-
},
|
|
48
|
-
{ key: '5', isViewable: true, item: { id: '5', type: 'Message' } },
|
|
49
|
-
],
|
|
50
|
-
})
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
expect(onSeen).toHaveBeenCalledTimes(1)
|
|
54
|
-
expect(onSeen).toHaveBeenCalledWith('5')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('does not fire before the user has scrolled', () => {
|
|
58
|
-
const onSeen = jest.fn()
|
|
59
|
-
const observer = reportViewableMessages<Msg>(onSeen)
|
|
60
|
-
|
|
61
|
-
observer(
|
|
62
|
-
event({
|
|
63
|
-
userHasScrolled: false,
|
|
64
|
-
viewableItems: [{ key: '10', isViewable: true, item: { id: '10', type: 'Message' } }],
|
|
65
|
-
})
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
expect(onSeen).not.toHaveBeenCalled()
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
describe('detectDividerExitTowardNewer', () => {
|
|
73
|
-
const baseArgs = { dividerKey: 'unread-divider', initialMessageId: '050' }
|
|
74
|
-
|
|
75
|
-
it('fires onExited when the divider leaves and only newer messages remain visible', () => {
|
|
76
|
-
const onExited = jest.fn()
|
|
77
|
-
const observer = detectDividerExitTowardNewer<{ id: string; type?: string }>({
|
|
78
|
-
...baseArgs,
|
|
79
|
-
onExited,
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
observer(
|
|
83
|
-
event({
|
|
84
|
-
changed: [
|
|
85
|
-
{
|
|
86
|
-
key: 'unread-divider',
|
|
87
|
-
isViewable: false,
|
|
88
|
-
item: { id: 'unread-divider', type: 'UnreadDivider' },
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
viewableItems: [
|
|
92
|
-
{ key: '055', isViewable: true, item: { id: '055', type: 'Message' } },
|
|
93
|
-
{ key: '056', isViewable: true, item: { id: '056', type: 'Message' } },
|
|
94
|
-
],
|
|
95
|
-
})
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
expect(onExited).toHaveBeenCalledTimes(1)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('does not fire when divider leaves toward older messages', () => {
|
|
102
|
-
const onExited = jest.fn()
|
|
103
|
-
const observer = detectDividerExitTowardNewer<{ id: string; type?: string }>({
|
|
104
|
-
...baseArgs,
|
|
105
|
-
onExited,
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
observer(
|
|
109
|
-
event({
|
|
110
|
-
changed: [
|
|
111
|
-
{
|
|
112
|
-
key: 'unread-divider',
|
|
113
|
-
isViewable: false,
|
|
114
|
-
item: { id: 'unread-divider', type: 'UnreadDivider' },
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
viewableItems: [
|
|
118
|
-
{ key: '045', isViewable: true, item: { id: '045', type: 'Message' } },
|
|
119
|
-
{ key: '048', isViewable: true, item: { id: '048', type: 'Message' } },
|
|
120
|
-
],
|
|
121
|
-
})
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
expect(onExited).not.toHaveBeenCalled()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('does not fire before the user has scrolled', () => {
|
|
128
|
-
const onExited = jest.fn()
|
|
129
|
-
const observer = detectDividerExitTowardNewer<{ id: string; type?: string }>({
|
|
130
|
-
...baseArgs,
|
|
131
|
-
onExited,
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
observer(
|
|
135
|
-
event({
|
|
136
|
-
userHasScrolled: false,
|
|
137
|
-
changed: [
|
|
138
|
-
{
|
|
139
|
-
key: 'unread-divider',
|
|
140
|
-
isViewable: false,
|
|
141
|
-
item: { id: 'unread-divider', type: 'UnreadDivider' },
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
viewableItems: [{ key: '055', isViewable: true, item: { id: '055', type: 'Message' } }],
|
|
145
|
-
})
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
expect(onExited).not.toHaveBeenCalled()
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('no-ops when initialMessageId is null (observer is always installed)', () => {
|
|
152
|
-
const onExited = jest.fn()
|
|
153
|
-
const observer = detectDividerExitTowardNewer<{ id: string; type?: string }>({
|
|
154
|
-
dividerKey: 'unread-divider',
|
|
155
|
-
initialMessageId: null,
|
|
156
|
-
onExited,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
observer(
|
|
160
|
-
event({
|
|
161
|
-
changed: [{ key: 'unread-divider', isViewable: false, item: { id: 'unread-divider' } }],
|
|
162
|
-
viewableItems: [{ key: '055', isViewable: true, item: { id: '055' } }],
|
|
163
|
-
})
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
expect(onExited).not.toHaveBeenCalled()
|
|
167
|
-
})
|
|
168
|
-
})
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { dividerExitedTowardNewer } from '../unread_divider_helpers'
|
|
2
|
-
|
|
3
|
-
const dividerKey = 'unread-divider'
|
|
4
|
-
const initialMessageId = '050'
|
|
5
|
-
const msg = (id: string) => ({ item: { id, type: 'Message' } })
|
|
6
|
-
|
|
7
|
-
describe('dividerExitedTowardNewer', () => {
|
|
8
|
-
it('returns false while the divider is still viewable', () => {
|
|
9
|
-
const result = dividerExitedTowardNewer({
|
|
10
|
-
changed: [{ key: dividerKey, isViewable: true }],
|
|
11
|
-
viewableItems: [
|
|
12
|
-
msg('049'),
|
|
13
|
-
{ item: { id: dividerKey, type: 'UnreadDivider' } },
|
|
14
|
-
msg('050'),
|
|
15
|
-
msg('051'),
|
|
16
|
-
],
|
|
17
|
-
dividerKey,
|
|
18
|
-
initialMessageId,
|
|
19
|
-
})
|
|
20
|
-
expect(result).toBe(false)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('returns true when divider exits and only newer messages are visible (scrolled toward newer)', () => {
|
|
24
|
-
const result = dividerExitedTowardNewer({
|
|
25
|
-
changed: [{ key: dividerKey, isViewable: false }],
|
|
26
|
-
viewableItems: [msg('055'), msg('056'), msg('057')],
|
|
27
|
-
dividerKey,
|
|
28
|
-
initialMessageId,
|
|
29
|
-
})
|
|
30
|
-
expect(result).toBe(true)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('returns false when divider exits and only older messages are visible (scrolled toward older)', () => {
|
|
34
|
-
const result = dividerExitedTowardNewer({
|
|
35
|
-
changed: [{ key: dividerKey, isViewable: false }],
|
|
36
|
-
viewableItems: [msg('040'), msg('045'), msg('048')],
|
|
37
|
-
dividerKey,
|
|
38
|
-
initialMessageId,
|
|
39
|
-
})
|
|
40
|
-
expect(result).toBe(false)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('returns false when divider exits but the visible window straddles the boundary', () => {
|
|
44
|
-
const result = dividerExitedTowardNewer({
|
|
45
|
-
changed: [{ key: dividerKey, isViewable: false }],
|
|
46
|
-
viewableItems: [msg('048'), msg('049'), msg('055')],
|
|
47
|
-
dividerKey,
|
|
48
|
-
initialMessageId,
|
|
49
|
-
})
|
|
50
|
-
expect(result).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('returns false when the divider entry is not in the changed set', () => {
|
|
54
|
-
const result = dividerExitedTowardNewer({
|
|
55
|
-
changed: [{ key: '055', isViewable: true }],
|
|
56
|
-
viewableItems: [msg('055'), msg('056')],
|
|
57
|
-
dividerKey,
|
|
58
|
-
initialMessageId,
|
|
59
|
-
})
|
|
60
|
-
expect(result).toBe(false)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('returns false when no message items are visible (only the divider was)', () => {
|
|
64
|
-
const result = dividerExitedTowardNewer({
|
|
65
|
-
changed: [{ key: dividerKey, isViewable: false }],
|
|
66
|
-
viewableItems: [],
|
|
67
|
-
dividerKey,
|
|
68
|
-
initialMessageId,
|
|
69
|
-
})
|
|
70
|
-
expect(result).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('ignores non-Message items (date separators, reply shadows) when deciding direction', () => {
|
|
74
|
-
const result = dividerExitedTowardNewer({
|
|
75
|
-
changed: [{ key: dividerKey, isViewable: false }],
|
|
76
|
-
viewableItems: [
|
|
77
|
-
{ item: { id: 'day-divider-200', type: 'DateSeparator' } },
|
|
78
|
-
{ item: { id: '055-rootA', type: 'ReplyShadowMessage' } },
|
|
79
|
-
],
|
|
80
|
-
dividerKey,
|
|
81
|
-
initialMessageId,
|
|
82
|
-
})
|
|
83
|
-
expect(result).toBe(false)
|
|
84
|
-
})
|
|
85
|
-
})
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { ApiCollection, MessageResource } from '../types'
|
|
2
|
-
import { RequestData } from './client'
|
|
3
|
-
|
|
4
|
-
export type MessagesPageParam = Partial<RequestData> & {
|
|
5
|
-
order?: 'asc' | 'desc'
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const anchoredSeedPageParams = (anchor: string): MessagesPageParam[] => [
|
|
9
|
-
{ where: { id_gte: anchor }, order: 'asc' },
|
|
10
|
-
{ where: { id_lt: anchor }, order: 'desc' },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
export const olderPageParam = (
|
|
14
|
-
page: ApiCollection<MessageResource>
|
|
15
|
-
): MessagesPageParam | undefined => {
|
|
16
|
-
const idLt = page.meta?.next?.idLt
|
|
17
|
-
if (!idLt) return undefined
|
|
18
|
-
return { where: { id_lt: idLt }, order: 'desc' }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const newerPageParam = (
|
|
22
|
-
page: ApiCollection<MessageResource>
|
|
23
|
-
): MessagesPageParam | undefined => {
|
|
24
|
-
const idGt = page.meta?.next?.idGt
|
|
25
|
-
if (!idGt) return undefined
|
|
26
|
-
return { where: { id_gt: idGt }, order: 'asc' }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const sortAndFilterMessages = (pages: ApiCollection<MessageResource>[]): MessageResource[] =>
|
|
30
|
-
pages
|
|
31
|
-
.flatMap(page => page.data)
|
|
32
|
-
.filter(
|
|
33
|
-
message =>
|
|
34
|
-
(!message.deletedAt || message.replyRootId) &&
|
|
35
|
-
(message.attachments?.length || message.text?.length)
|
|
36
|
-
)
|
|
37
|
-
.sort((a, b) => -a.id.localeCompare(b.id))
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export const FLUSH_DELAY_MS = 2000
|
|
2
|
-
|
|
3
|
-
type SendFn = (args: { conversationId: number; sortKey: string }) => void
|
|
4
|
-
|
|
5
|
-
export function makeHighestSeenTracker(
|
|
6
|
-
conversationId: number,
|
|
7
|
-
send: SendFn,
|
|
8
|
-
flushDelayMs: number = FLUSH_DELAY_MS
|
|
9
|
-
) {
|
|
10
|
-
let highest: string | null = null
|
|
11
|
-
let lastSent: string | null = null
|
|
12
|
-
let timer: ReturnType<typeof setTimeout> | null = null
|
|
13
|
-
|
|
14
|
-
const fire = () => {
|
|
15
|
-
timer = null
|
|
16
|
-
if (!highest || highest === lastSent) return
|
|
17
|
-
lastSent = highest
|
|
18
|
-
send({ conversationId, sortKey: highest })
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
onSeen(sortKey: string) {
|
|
23
|
-
if (highest && sortKey.localeCompare(highest) <= 0) return
|
|
24
|
-
highest = sortKey
|
|
25
|
-
if (timer) clearTimeout(timer)
|
|
26
|
-
timer = setTimeout(fire, flushDelayMs)
|
|
27
|
-
},
|
|
28
|
-
flushNow() {
|
|
29
|
-
if (timer) {
|
|
30
|
-
clearTimeout(timer)
|
|
31
|
-
timer = null
|
|
32
|
-
}
|
|
33
|
-
fire()
|
|
34
|
-
},
|
|
35
|
-
cancel() {
|
|
36
|
-
if (timer) {
|
|
37
|
-
clearTimeout(timer)
|
|
38
|
-
timer = null
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { dividerExitedTowardNewer } from './unread_divider_helpers'
|
|
2
|
-
|
|
3
|
-
export interface ViewableEntry<Item> {
|
|
4
|
-
key: string
|
|
5
|
-
isViewable: boolean
|
|
6
|
-
item: Item
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ViewabilityEvent<Item> {
|
|
10
|
-
viewableItems: ViewableEntry<Item>[]
|
|
11
|
-
changed: ViewableEntry<Item>[]
|
|
12
|
-
userHasScrolled: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type ViewabilityObserver<Item> = (event: ViewabilityEvent<Item>) => void
|
|
16
|
-
|
|
17
|
-
export function reportViewableMessages<Item extends { id?: string; type?: string }>(
|
|
18
|
-
onMessageSeen: (id: string) => void
|
|
19
|
-
): ViewabilityObserver<Item> {
|
|
20
|
-
return ({ viewableItems, userHasScrolled }) => {
|
|
21
|
-
if (!userHasScrolled) return
|
|
22
|
-
for (const entry of viewableItems) {
|
|
23
|
-
if (entry.item?.type !== 'Message') continue
|
|
24
|
-
const id = entry.item?.id
|
|
25
|
-
if (typeof id === 'string') onMessageSeen(id)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function detectDividerExitTowardNewer<Item extends { id?: string; type?: string }>({
|
|
31
|
-
dividerKey,
|
|
32
|
-
initialMessageId,
|
|
33
|
-
onExited,
|
|
34
|
-
}: {
|
|
35
|
-
dividerKey: string
|
|
36
|
-
initialMessageId: string | null
|
|
37
|
-
onExited: () => void
|
|
38
|
-
}): ViewabilityObserver<Item> {
|
|
39
|
-
return ({ viewableItems, changed, userHasScrolled }) => {
|
|
40
|
-
if (!userHasScrolled || !initialMessageId) return
|
|
41
|
-
const exited = dividerExitedTowardNewer({
|
|
42
|
-
changed,
|
|
43
|
-
viewableItems,
|
|
44
|
-
dividerKey,
|
|
45
|
-
initialMessageId,
|
|
46
|
-
})
|
|
47
|
-
if (exited) onExited()
|
|
48
|
-
}
|
|
49
|
-
}
|