@planningcenter/chat-react-native 3.44.1-qa-875.0 → 3.44.1-qa-876.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/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +8 -1
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +7 -1
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js +8 -1
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/components/display/badge.d.ts.map +1 -1
- package/build/components/display/badge.js +15 -4
- package/build/components/display/badge.js.map +1 -1
- package/build/hooks/use_conversation_layout_diagnostics.d.ts +21 -0
- package/build/hooks/use_conversation_layout_diagnostics.d.ts.map +1 -0
- package/build/hooks/use_conversation_layout_diagnostics.js +99 -0
- package/build/hooks/use_conversation_layout_diagnostics.js.map +1 -0
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +16 -6
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/message_report_screen.d.ts.map +1 -1
- package/build/screens/message_report_screen.js +2 -4
- package/build/screens/message_report_screen.js.map +1 -1
- package/build/utils/aspect_ratio.d.ts.map +1 -1
- package/build/utils/aspect_ratio.js +4 -3
- package/build/utils/aspect_ratio.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/hooks/use_conversation_layout_diagnostics.test.tsx +142 -0
- package/src/components/conversation/message.tsx +8 -1
- package/src/components/conversation/message_form.tsx +14 -2
- package/src/components/conversation/reply_shadow_message.tsx +8 -1
- package/src/components/display/badge.tsx +19 -3
- package/src/hooks/use_conversation_layout_diagnostics.ts +123 -0
- package/src/screens/conversation_screen.tsx +19 -5
- package/src/screens/message_report_screen.tsx +6 -4
- package/src/utils/aspect_ratio.ts +4 -3
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
import { LayoutChangeEvent, PixelRatio, Platform, useWindowDimensions } from 'react-native'
|
|
3
|
+
import { Log } from '../utils'
|
|
4
|
+
|
|
5
|
+
const BREADCRUMB_INTERVAL_MS = 500
|
|
6
|
+
const MAX_BREADCRUMB_FLUSHES = 6
|
|
7
|
+
const SUSPICIOUS_MARGIN = 1
|
|
8
|
+
|
|
9
|
+
type LayoutSample = { width: number; height: number; x: number }
|
|
10
|
+
|
|
11
|
+
// Temporary instrumentation for CHAT-476 (Android-only `Layout: -2 < 0`
|
|
12
|
+
// conversation crash). Android's text layout hard-throws when a Text or
|
|
13
|
+
// TextInput resolves to a non-positive content width; this attaches to
|
|
14
|
+
// onLayout on suspect nodes and reports their resolved width through the
|
|
15
|
+
// host error reporter, so a QA repro surfaces which node it is before the
|
|
16
|
+
// native crash tears down the surface. Remove once CHAT-476 is resolved —
|
|
17
|
+
// this hook and every call site are tagged CHAT-476 for that reason.
|
|
18
|
+
|
|
19
|
+
const samplesByLabel = new Map<string, LayoutSample>()
|
|
20
|
+
const reportedSuspiciousLabels = new Set<string>()
|
|
21
|
+
|
|
22
|
+
// Test-only: the maps above are module-scope so useConversationLayoutDiagnostics
|
|
23
|
+
// can flush every probe's latest sample in one breadcrumb. Reset between tests.
|
|
24
|
+
export const __resetLayoutDiagnosticsForTests = () => {
|
|
25
|
+
samplesByLabel.clear()
|
|
26
|
+
reportedSuspiciousLabels.clear()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attaches to a suspect Text/TextInput/View's onLayout and reports through
|
|
31
|
+
* Log.reportError the first time its resolved width drops to at or below
|
|
32
|
+
* its own horizontal inset (padding/border) — the shape that Android's
|
|
33
|
+
* unclamped text-measure path can turn into a negative width. Returns
|
|
34
|
+
* undefined on non-Android platforms so it's a no-op there.
|
|
35
|
+
*/
|
|
36
|
+
export const useLayoutWidthProbe = (label: string, inset = 0) => {
|
|
37
|
+
const mountedAtRef = useRef(Date.now())
|
|
38
|
+
|
|
39
|
+
const handleLayout = useCallback(
|
|
40
|
+
(event: LayoutChangeEvent) => {
|
|
41
|
+
const { width, height, x } = event.nativeEvent.layout
|
|
42
|
+
const previous = samplesByLabel.get(label)
|
|
43
|
+
if (previous && previous.width === width && previous.height === height && previous.x === x) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
samplesByLabel.set(label, { width, height, x })
|
|
47
|
+
|
|
48
|
+
const isSuspicious = width <= inset + SUSPICIOUS_MARGIN
|
|
49
|
+
if (!isSuspicious || reportedSuspiciousLabels.has(label)) return
|
|
50
|
+
|
|
51
|
+
reportedSuspiciousLabels.add(label)
|
|
52
|
+
Log.reportError(new Error(`CHAT-476: ${label} measured at or below its horizontal inset`), {
|
|
53
|
+
scope: 'component',
|
|
54
|
+
tags: { team: 'chat', chat476: 'near-zero-width' },
|
|
55
|
+
extra: {
|
|
56
|
+
label,
|
|
57
|
+
width,
|
|
58
|
+
height,
|
|
59
|
+
x,
|
|
60
|
+
inset,
|
|
61
|
+
msSinceMount: Date.now() - mountedAtRef.current,
|
|
62
|
+
pixelRatio: PixelRatio.get(),
|
|
63
|
+
fontScale: PixelRatio.getFontScale(),
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
[label, inset]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (Platform.OS !== 'android') return undefined
|
|
71
|
+
|
|
72
|
+
return handleLayout
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Mount once per conversation screen. Periodically flushes every probed
|
|
77
|
+
* label's latest known width as a single breadcrumb, so there's a timeline
|
|
78
|
+
* leading up to the crash even if no individual probe crosses the
|
|
79
|
+
* suspicious threshold. Capped at MAX_BREADCRUMB_FLUSHES per mount.
|
|
80
|
+
*/
|
|
81
|
+
export const useConversationLayoutDiagnostics = ({
|
|
82
|
+
conversationId,
|
|
83
|
+
replyRootId,
|
|
84
|
+
}: {
|
|
85
|
+
conversationId: number
|
|
86
|
+
replyRootId?: string | null
|
|
87
|
+
}) => {
|
|
88
|
+
const { width: windowWidth } = useWindowDimensions()
|
|
89
|
+
const mountedAtRef = useRef(Date.now())
|
|
90
|
+
const flushCountRef = useRef(0)
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (Platform.OS !== 'android') return
|
|
94
|
+
|
|
95
|
+
samplesByLabel.clear()
|
|
96
|
+
reportedSuspiciousLabels.clear()
|
|
97
|
+
mountedAtRef.current = Date.now()
|
|
98
|
+
flushCountRef.current = 0
|
|
99
|
+
|
|
100
|
+
const interval = setInterval(() => {
|
|
101
|
+
if (flushCountRef.current >= MAX_BREADCRUMB_FLUSHES || samplesByLabel.size === 0) return
|
|
102
|
+
|
|
103
|
+
flushCountRef.current += 1
|
|
104
|
+
Log.reportError(new Error('CHAT-476: layout breadcrumb'), {
|
|
105
|
+
scope: 'component',
|
|
106
|
+
tags: { team: 'chat', chat476: 'breadcrumb' },
|
|
107
|
+
extra: {
|
|
108
|
+
conversationId,
|
|
109
|
+
replyRootId,
|
|
110
|
+
msSinceMount: Date.now() - mountedAtRef.current,
|
|
111
|
+
windowWidth,
|
|
112
|
+
pixelRatio: PixelRatio.get(),
|
|
113
|
+
fontScale: PixelRatio.getFontScale(),
|
|
114
|
+
samples: [...samplesByLabel.entries()].map(
|
|
115
|
+
([label, { width, height, x }]) => `${label}|w=${width}|h=${height}|x=${x}`
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
}, BREADCRUMB_INTERVAL_MS)
|
|
120
|
+
|
|
121
|
+
return () => clearInterval(interval)
|
|
122
|
+
}, [conversationId, replyRootId, windowWidth])
|
|
123
|
+
}
|
|
@@ -32,6 +32,11 @@ import { useTheme } from '../hooks'
|
|
|
32
32
|
import { useMessageAvailableMembersAction } from '../hooks/direct_messages/use_message_available_members_action'
|
|
33
33
|
import { useConversation } from '../hooks/use_conversation'
|
|
34
34
|
import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
|
|
35
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
36
|
+
import {
|
|
37
|
+
useConversationLayoutDiagnostics,
|
|
38
|
+
useLayoutWidthProbe,
|
|
39
|
+
} from '../hooks/use_conversation_layout_diagnostics'
|
|
35
40
|
import { useConversationMessages } from '../hooks/use_conversation_messages'
|
|
36
41
|
import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
|
|
37
42
|
import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
|
|
@@ -97,6 +102,8 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
97
102
|
})
|
|
98
103
|
useConversationJoltEvents({ conversationId: conversation_id })
|
|
99
104
|
useConversationMessagesJoltEvents({ conversationId: conversation_id })
|
|
105
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
106
|
+
useConversationLayoutDiagnostics({ conversationId: conversation_id, replyRootId: reply_root_id })
|
|
100
107
|
const { markHandledByMessageForm } = useVideoModerationJoltEvents()
|
|
101
108
|
useEnsureConversationsRouteExists()
|
|
102
109
|
useMarkLatestMessageRead({ conversation, messages })
|
|
@@ -217,8 +224,8 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
217
224
|
)
|
|
218
225
|
}}
|
|
219
226
|
onEndReached={() => {
|
|
220
|
-
//
|
|
221
|
-
//
|
|
227
|
+
// VirtualizedList dedupes onEndReached per content length, not
|
|
228
|
+
// per fetch, so it can refire while a page is already in flight.
|
|
222
229
|
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
|
|
223
230
|
}}
|
|
224
231
|
ListHeaderComponent={<View style={styles.listHeader} />}
|
|
@@ -293,11 +300,13 @@ function InlineDateSeparator({ date }: DateSeparator) {
|
|
|
293
300
|
const { isThisYear } = getRelativeDateStatus(date)
|
|
294
301
|
const showYear = !isThisYear
|
|
295
302
|
const dateStamp = formatDate(date, { style: 'long', year: showYear })
|
|
303
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
304
|
+
const onDateTextLayout = useLayoutWidthProbe('dateSeparator.text', 16)
|
|
296
305
|
|
|
297
306
|
return (
|
|
298
307
|
<View style={styles.container}>
|
|
299
308
|
<View style={styles.separator} />
|
|
300
|
-
<Text variant="footnote" style={styles.dateText}>
|
|
309
|
+
<Text variant="footnote" style={styles.dateText} onLayout={onDateTextLayout}>
|
|
301
310
|
{dateStamp}
|
|
302
311
|
</Text>
|
|
303
312
|
<View style={styles.separator} />
|
|
@@ -346,11 +355,14 @@ export const ConversationScreenTitle = ({
|
|
|
346
355
|
const resourceType = badge?.pcoResourceType || ''
|
|
347
356
|
const productName = badge?.appName
|
|
348
357
|
const name = badge?.text || undefined
|
|
358
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
359
|
+
const onHeaderContainerLayout = useLayoutWidthProbe('header.container', 0)
|
|
349
360
|
|
|
350
361
|
return (
|
|
351
362
|
<PlatformPressable
|
|
352
363
|
accessibilityHint="Opens details about members and settings"
|
|
353
364
|
style={styles.container}
|
|
365
|
+
onLayout={onHeaderContainerLayout}
|
|
354
366
|
onPress={() => {
|
|
355
367
|
if (deleted) return
|
|
356
368
|
|
|
@@ -382,8 +394,10 @@ const usePressableHeaderStyle = () => {
|
|
|
382
394
|
const { width } = useWindowDimensions()
|
|
383
395
|
const slotReserve = useHeaderSlotReserve()
|
|
384
396
|
|
|
385
|
-
// Guards split-screen
|
|
386
|
-
// available width
|
|
397
|
+
// Guards narrow windows (split-screen, freeform, DeX) where slotReserve * 2
|
|
398
|
+
// exceeds the available width. A negative maxWidth can hand a descendant
|
|
399
|
+
// <Text> a negative measure width, and android.text.Layout hard-throws on
|
|
400
|
+
// that (`Layout: -2 < 0`) rather than clamping the way iOS does.
|
|
387
401
|
const headerContainerStyle = { maxWidth: Math.max(0, width - slotReserve * 2) }
|
|
388
402
|
|
|
389
403
|
return StyleSheet.create({
|
|
@@ -30,9 +30,11 @@ export const MessageReportScreenOptions = ({
|
|
|
30
30
|
}: NativeStackScreenProps<any>): NativeStackNavigationOptions => {
|
|
31
31
|
const handleClose = () =>
|
|
32
32
|
navigation.dispatch(
|
|
33
|
-
StackActions.popTo(
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
StackActions.popTo(
|
|
34
|
+
'Conversation',
|
|
35
|
+
{ conversation_id: route.params?.conversation_id },
|
|
36
|
+
{ merge: true }
|
|
37
|
+
)
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
return composeOptions(
|
|
@@ -100,7 +102,7 @@ function MessageReportScreenContent({
|
|
|
100
102
|
}, [isFormValid, selectedReason, reasonDetail, submitReport])
|
|
101
103
|
|
|
102
104
|
const handleClose = useCallback(() => {
|
|
103
|
-
navigation.dispatch(StackActions.popTo('Conversation', { conversation_id }))
|
|
105
|
+
navigation.dispatch(StackActions.popTo('Conversation', { conversation_id }, { merge: true }))
|
|
104
106
|
}, [navigation, conversation_id])
|
|
105
107
|
|
|
106
108
|
const formVisible = status !== 'pending' && status !== 'success'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// assertKeysAreNumbers coerces unparseable attachment dimensions to 0, so a
|
|
2
|
+
// raw width / height can be NaN or Infinity. Yoga ignores a NaN aspectRatio
|
|
3
|
+
// and resolves Infinity by collapsing the dependent dimension to 0, so both
|
|
4
|
+
// produce a silently broken box rather than an error.
|
|
4
5
|
export const safeAspectRatio = (
|
|
5
6
|
width: number | undefined,
|
|
6
7
|
height: number | undefined,
|