@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.
Files changed (35) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +8 -1
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_form.d.ts.map +1 -1
  5. package/build/components/conversation/message_form.js +7 -1
  6. package/build/components/conversation/message_form.js.map +1 -1
  7. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  8. package/build/components/conversation/reply_shadow_message.js +8 -1
  9. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  10. package/build/components/display/badge.d.ts.map +1 -1
  11. package/build/components/display/badge.js +15 -4
  12. package/build/components/display/badge.js.map +1 -1
  13. package/build/hooks/use_conversation_layout_diagnostics.d.ts +21 -0
  14. package/build/hooks/use_conversation_layout_diagnostics.d.ts.map +1 -0
  15. package/build/hooks/use_conversation_layout_diagnostics.js +99 -0
  16. package/build/hooks/use_conversation_layout_diagnostics.js.map +1 -0
  17. package/build/screens/conversation_screen.d.ts.map +1 -1
  18. package/build/screens/conversation_screen.js +16 -6
  19. package/build/screens/conversation_screen.js.map +1 -1
  20. package/build/screens/message_report_screen.d.ts.map +1 -1
  21. package/build/screens/message_report_screen.js +2 -4
  22. package/build/screens/message_report_screen.js.map +1 -1
  23. package/build/utils/aspect_ratio.d.ts.map +1 -1
  24. package/build/utils/aspect_ratio.js +4 -3
  25. package/build/utils/aspect_ratio.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/__tests__/hooks/use_conversation_layout_diagnostics.test.tsx +142 -0
  28. package/src/components/conversation/message.tsx +8 -1
  29. package/src/components/conversation/message_form.tsx +14 -2
  30. package/src/components/conversation/reply_shadow_message.tsx +8 -1
  31. package/src/components/display/badge.tsx +19 -3
  32. package/src/hooks/use_conversation_layout_diagnostics.ts +123 -0
  33. package/src/screens/conversation_screen.tsx +19 -5
  34. package/src/screens/message_report_screen.tsx +6 -4
  35. 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
- // Android's inverted FlatList can refire onEndReached while a
221
- // page fetch is already in flight, queueing unbounded requests.
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/DeX widths where slotReserve * 2 exceeds the
386
- // available width, which would otherwise resolve to a negative maxWidth.
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('Conversation', {
34
- conversation_id: route.params?.conversation_id,
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
- // Android hard-throws when a <Text>/<Image> is measured with a non-positive
2
- // resolved width (`Layout: -2 < 0`), so any aspectRatio derived from
3
- // attachment metadata must never resolve to 0, negative, or non-finite.
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,