@planningcenter/chat-react-native 3.35.0-rc.3 → 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.map +1 -1
- package/build/screens/conversation_screen.js +87 -44
- 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 +9 -2
- package/build/utils/group_messages.d.ts.map +1 -1
- package/build/utils/group_messages.js +20 -1
- package/build/utils/group_messages.js.map +1 -1
- 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 -70
- package/src/utils/__tests__/group_messages.test.ts +71 -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 +42 -2
- 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,85 @@
|
|
|
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
|
+
})
|
|
@@ -131,6 +131,11 @@ 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
|
+
|
|
134
139
|
export function mergeMessageUpdate(
|
|
135
140
|
record: MessageResource,
|
|
136
141
|
current?: MessageResource
|
|
@@ -4,8 +4,12 @@ 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
|
+
|
|
7
9
|
export type DateSeparator = { type: 'DateSeparator'; id: string; date: string }
|
|
8
10
|
|
|
11
|
+
export type UnreadDividerItem = { type: 'UnreadDivider'; id: typeof UNREAD_DIVIDER_KEY }
|
|
12
|
+
|
|
9
13
|
export type ReplyShadowMessage = {
|
|
10
14
|
type: 'ReplyShadowMessage'
|
|
11
15
|
id: string
|
|
@@ -14,14 +18,25 @@ export type ReplyShadowMessage = {
|
|
|
14
18
|
nextRendersAuthor: boolean
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
export type EnrichedMessage =
|
|
21
|
+
export type EnrichedMessage =
|
|
22
|
+
| MessageResource
|
|
23
|
+
| DateSeparator
|
|
24
|
+
| UnreadDividerItem
|
|
25
|
+
| ReplyShadowMessage
|
|
18
26
|
|
|
19
27
|
interface GroupMessagesProps {
|
|
20
28
|
ms: MessageResource[]
|
|
21
29
|
inReplyScreen?: boolean
|
|
30
|
+
jumpToUnreadActive?: boolean
|
|
31
|
+
initialMessageId?: string | null
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
export function groupMessages({
|
|
34
|
+
export function groupMessages({
|
|
35
|
+
ms,
|
|
36
|
+
inReplyScreen,
|
|
37
|
+
jumpToUnreadActive,
|
|
38
|
+
initialMessageId,
|
|
39
|
+
}: GroupMessagesProps): EnrichedMessage[] {
|
|
25
40
|
const items: EnrichedMessage[] = []
|
|
26
41
|
let myLatestSeen = false
|
|
27
42
|
let nextNeighborEnriched: MessageResource | undefined
|
|
@@ -33,6 +48,9 @@ export function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): Enrich
|
|
|
33
48
|
if (isSystemMessage(message)) {
|
|
34
49
|
const enriched = enrichSystemMessage(message, next)
|
|
35
50
|
items.push(enriched)
|
|
51
|
+
if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {
|
|
52
|
+
items.push(unreadDivider())
|
|
53
|
+
}
|
|
36
54
|
if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))
|
|
37
55
|
nextNeighborEnriched = enriched
|
|
38
56
|
return
|
|
@@ -44,6 +62,10 @@ export function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): Enrich
|
|
|
44
62
|
const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen)
|
|
45
63
|
items.push(enriched)
|
|
46
64
|
|
|
65
|
+
if (crossesUnreadBoundary(message, prev, jumpToUnreadActive, initialMessageId)) {
|
|
66
|
+
items.push(unreadDivider())
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
const shadow = replyShadowFor(enriched, prev)
|
|
48
70
|
if (shadow) items.push(shadow)
|
|
49
71
|
|
|
@@ -57,6 +79,24 @@ export function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): Enrich
|
|
|
57
79
|
return items
|
|
58
80
|
}
|
|
59
81
|
|
|
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
|
+
|
|
60
100
|
function neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {
|
|
61
101
|
return { prev: arr[i + 1], next: arr[i - 1] }
|
|
62
102
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type ViewableChangeEntry = { key: string; isViewable: boolean }
|
|
2
|
+
type ViewableItem = { item: { id?: string; type?: string } }
|
|
3
|
+
|
|
4
|
+
export function dividerExitedTowardNewer({
|
|
5
|
+
changed,
|
|
6
|
+
viewableItems,
|
|
7
|
+
dividerKey,
|
|
8
|
+
initialMessageId,
|
|
9
|
+
}: {
|
|
10
|
+
changed: ViewableChangeEntry[]
|
|
11
|
+
viewableItems: ViewableItem[]
|
|
12
|
+
dividerKey: string
|
|
13
|
+
initialMessageId: string
|
|
14
|
+
}): boolean {
|
|
15
|
+
const dividerExited = changed.some(c => c.key === dividerKey && !c.isViewable)
|
|
16
|
+
if (!dividerExited) return false
|
|
17
|
+
|
|
18
|
+
const visibleMessageIds = viewableItems
|
|
19
|
+
.filter(v => v.item?.type === 'Message')
|
|
20
|
+
.map(v => v.item?.id)
|
|
21
|
+
.filter((id): id is string => !!id)
|
|
22
|
+
if (visibleMessageIds.length === 0) return false
|
|
23
|
+
|
|
24
|
+
return visibleMessageIds.every(id => id.localeCompare(initialMessageId) > 0)
|
|
25
|
+
}
|