@planningcenter/chat-react-native 3.44.1-qa-876.0 → 3.44.1-qa-staging.1
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/attachments/giphy_attachment.js +2 -2
- package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
- package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/image_attachment.js +14 -15
- package/build/components/conversation/attachments/image_attachment.js.map +1 -1
- package/build/components/conversation/attachments/video_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/video_attachment.js +1 -2
- package/build/components/conversation/attachments/video_attachment.js.map +1 -1
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +4 -9
- 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 +6 -10
- 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 +6 -11
- 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 +3 -16
- package/build/components/display/badge.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts +0 -1
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +2 -10
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +5 -22
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/utils/index.d.ts +0 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +0 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/message_text.d.ts +3 -0
- package/build/utils/message_text.d.ts.map +1 -0
- package/build/utils/message_text.js +8 -0
- package/build/utils/message_text.js.map +1 -0
- package/package.json +3 -3
- package/src/components/conversation/attachments/giphy_attachment.tsx +2 -2
- package/src/components/conversation/attachments/image_attachment.tsx +26 -30
- package/src/components/conversation/attachments/video_attachment.tsx +1 -2
- package/src/components/conversation/message.tsx +4 -9
- package/src/components/conversation/message_form.tsx +8 -17
- package/src/components/conversation/reply_shadow_message.tsx +6 -11
- package/src/components/display/badge.tsx +2 -20
- package/src/hooks/use_conversation_messages.ts +2 -10
- package/src/screens/conversation_screen.tsx +4 -25
- package/src/utils/__tests__/message_text.test.ts +45 -0
- package/src/utils/index.ts +0 -1
- package/src/utils/message_text.ts +9 -0
- package/build/hooks/use_conversation_layout_diagnostics.d.ts +0 -21
- package/build/hooks/use_conversation_layout_diagnostics.d.ts.map +0 -1
- package/build/hooks/use_conversation_layout_diagnostics.js +0 -99
- package/build/hooks/use_conversation_layout_diagnostics.js.map +0 -1
- package/build/utils/aspect_ratio.d.ts +0 -2
- package/build/utils/aspect_ratio.d.ts.map +0 -1
- package/build/utils/aspect_ratio.js +0 -11
- package/build/utils/aspect_ratio.js.map +0 -1
- package/src/__tests__/hooks/use_conversation_layout_diagnostics.test.tsx +0 -142
- package/src/hooks/use_conversation_layout_diagnostics.ts +0 -123
- package/src/utils/__tests__/aspect_ratio.test.ts +0 -50
- package/src/utils/aspect_ratio.ts +0 -14
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
-
import { PixelRatio, Platform, useWindowDimensions } from 'react-native';
|
|
3
|
-
import { Log } from '../utils';
|
|
4
|
-
const BREADCRUMB_INTERVAL_MS = 500;
|
|
5
|
-
const MAX_BREADCRUMB_FLUSHES = 6;
|
|
6
|
-
const SUSPICIOUS_MARGIN = 1;
|
|
7
|
-
// Temporary instrumentation for CHAT-476 (Android-only `Layout: -2 < 0`
|
|
8
|
-
// conversation crash). Android's text layout hard-throws when a Text or
|
|
9
|
-
// TextInput resolves to a non-positive content width; this attaches to
|
|
10
|
-
// onLayout on suspect nodes and reports their resolved width through the
|
|
11
|
-
// host error reporter, so a QA repro surfaces which node it is before the
|
|
12
|
-
// native crash tears down the surface. Remove once CHAT-476 is resolved —
|
|
13
|
-
// this hook and every call site are tagged CHAT-476 for that reason.
|
|
14
|
-
const samplesByLabel = new Map();
|
|
15
|
-
const reportedSuspiciousLabels = new Set();
|
|
16
|
-
// Test-only: the maps above are module-scope so useConversationLayoutDiagnostics
|
|
17
|
-
// can flush every probe's latest sample in one breadcrumb. Reset between tests.
|
|
18
|
-
export const __resetLayoutDiagnosticsForTests = () => {
|
|
19
|
-
samplesByLabel.clear();
|
|
20
|
-
reportedSuspiciousLabels.clear();
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Attaches to a suspect Text/TextInput/View's onLayout and reports through
|
|
24
|
-
* Log.reportError the first time its resolved width drops to at or below
|
|
25
|
-
* its own horizontal inset (padding/border) — the shape that Android's
|
|
26
|
-
* unclamped text-measure path can turn into a negative width. Returns
|
|
27
|
-
* undefined on non-Android platforms so it's a no-op there.
|
|
28
|
-
*/
|
|
29
|
-
export const useLayoutWidthProbe = (label, inset = 0) => {
|
|
30
|
-
const mountedAtRef = useRef(Date.now());
|
|
31
|
-
const handleLayout = useCallback((event) => {
|
|
32
|
-
const { width, height, x } = event.nativeEvent.layout;
|
|
33
|
-
const previous = samplesByLabel.get(label);
|
|
34
|
-
if (previous && previous.width === width && previous.height === height && previous.x === x) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
samplesByLabel.set(label, { width, height, x });
|
|
38
|
-
const isSuspicious = width <= inset + SUSPICIOUS_MARGIN;
|
|
39
|
-
if (!isSuspicious || reportedSuspiciousLabels.has(label))
|
|
40
|
-
return;
|
|
41
|
-
reportedSuspiciousLabels.add(label);
|
|
42
|
-
Log.reportError(new Error(`CHAT-476: ${label} measured at or below its horizontal inset`), {
|
|
43
|
-
scope: 'component',
|
|
44
|
-
tags: { team: 'chat', chat476: 'near-zero-width' },
|
|
45
|
-
extra: {
|
|
46
|
-
label,
|
|
47
|
-
width,
|
|
48
|
-
height,
|
|
49
|
-
x,
|
|
50
|
-
inset,
|
|
51
|
-
msSinceMount: Date.now() - mountedAtRef.current,
|
|
52
|
-
pixelRatio: PixelRatio.get(),
|
|
53
|
-
fontScale: PixelRatio.getFontScale(),
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
}, [label, inset]);
|
|
57
|
-
if (Platform.OS !== 'android')
|
|
58
|
-
return undefined;
|
|
59
|
-
return handleLayout;
|
|
60
|
-
};
|
|
61
|
-
/**
|
|
62
|
-
* Mount once per conversation screen. Periodically flushes every probed
|
|
63
|
-
* label's latest known width as a single breadcrumb, so there's a timeline
|
|
64
|
-
* leading up to the crash even if no individual probe crosses the
|
|
65
|
-
* suspicious threshold. Capped at MAX_BREADCRUMB_FLUSHES per mount.
|
|
66
|
-
*/
|
|
67
|
-
export const useConversationLayoutDiagnostics = ({ conversationId, replyRootId, }) => {
|
|
68
|
-
const { width: windowWidth } = useWindowDimensions();
|
|
69
|
-
const mountedAtRef = useRef(Date.now());
|
|
70
|
-
const flushCountRef = useRef(0);
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (Platform.OS !== 'android')
|
|
73
|
-
return;
|
|
74
|
-
samplesByLabel.clear();
|
|
75
|
-
reportedSuspiciousLabels.clear();
|
|
76
|
-
mountedAtRef.current = Date.now();
|
|
77
|
-
flushCountRef.current = 0;
|
|
78
|
-
const interval = setInterval(() => {
|
|
79
|
-
if (flushCountRef.current >= MAX_BREADCRUMB_FLUSHES || samplesByLabel.size === 0)
|
|
80
|
-
return;
|
|
81
|
-
flushCountRef.current += 1;
|
|
82
|
-
Log.reportError(new Error('CHAT-476: layout breadcrumb'), {
|
|
83
|
-
scope: 'component',
|
|
84
|
-
tags: { team: 'chat', chat476: 'breadcrumb' },
|
|
85
|
-
extra: {
|
|
86
|
-
conversationId,
|
|
87
|
-
replyRootId,
|
|
88
|
-
msSinceMount: Date.now() - mountedAtRef.current,
|
|
89
|
-
windowWidth,
|
|
90
|
-
pixelRatio: PixelRatio.get(),
|
|
91
|
-
fontScale: PixelRatio.getFontScale(),
|
|
92
|
-
samples: [...samplesByLabel.entries()].map(([label, { width, height, x }]) => `${label}|w=${width}|h=${height}|x=${x}`),
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
}, BREADCRUMB_INTERVAL_MS);
|
|
96
|
-
return () => clearInterval(interval);
|
|
97
|
-
}, [conversationId, replyRootId, windowWidth]);
|
|
98
|
-
};
|
|
99
|
-
//# sourceMappingURL=use_conversation_layout_diagnostics.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use_conversation_layout_diagnostics.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_layout_diagnostics.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef } from 'react'\nimport { LayoutChangeEvent, PixelRatio, Platform, useWindowDimensions } from 'react-native'\nimport { Log } from '../utils'\n\nconst BREADCRUMB_INTERVAL_MS = 500\nconst MAX_BREADCRUMB_FLUSHES = 6\nconst SUSPICIOUS_MARGIN = 1\n\ntype LayoutSample = { width: number; height: number; x: number }\n\n// Temporary instrumentation for CHAT-476 (Android-only `Layout: -2 < 0`\n// conversation crash). Android's text layout hard-throws when a Text or\n// TextInput resolves to a non-positive content width; this attaches to\n// onLayout on suspect nodes and reports their resolved width through the\n// host error reporter, so a QA repro surfaces which node it is before the\n// native crash tears down the surface. Remove once CHAT-476 is resolved —\n// this hook and every call site are tagged CHAT-476 for that reason.\n\nconst samplesByLabel = new Map<string, LayoutSample>()\nconst reportedSuspiciousLabels = new Set<string>()\n\n// Test-only: the maps above are module-scope so useConversationLayoutDiagnostics\n// can flush every probe's latest sample in one breadcrumb. Reset between tests.\nexport const __resetLayoutDiagnosticsForTests = () => {\n samplesByLabel.clear()\n reportedSuspiciousLabels.clear()\n}\n\n/**\n * Attaches to a suspect Text/TextInput/View's onLayout and reports through\n * Log.reportError the first time its resolved width drops to at or below\n * its own horizontal inset (padding/border) — the shape that Android's\n * unclamped text-measure path can turn into a negative width. Returns\n * undefined on non-Android platforms so it's a no-op there.\n */\nexport const useLayoutWidthProbe = (label: string, inset = 0) => {\n const mountedAtRef = useRef(Date.now())\n\n const handleLayout = useCallback(\n (event: LayoutChangeEvent) => {\n const { width, height, x } = event.nativeEvent.layout\n const previous = samplesByLabel.get(label)\n if (previous && previous.width === width && previous.height === height && previous.x === x) {\n return\n }\n samplesByLabel.set(label, { width, height, x })\n\n const isSuspicious = width <= inset + SUSPICIOUS_MARGIN\n if (!isSuspicious || reportedSuspiciousLabels.has(label)) return\n\n reportedSuspiciousLabels.add(label)\n Log.reportError(new Error(`CHAT-476: ${label} measured at or below its horizontal inset`), {\n scope: 'component',\n tags: { team: 'chat', chat476: 'near-zero-width' },\n extra: {\n label,\n width,\n height,\n x,\n inset,\n msSinceMount: Date.now() - mountedAtRef.current,\n pixelRatio: PixelRatio.get(),\n fontScale: PixelRatio.getFontScale(),\n },\n })\n },\n [label, inset]\n )\n\n if (Platform.OS !== 'android') return undefined\n\n return handleLayout\n}\n\n/**\n * Mount once per conversation screen. Periodically flushes every probed\n * label's latest known width as a single breadcrumb, so there's a timeline\n * leading up to the crash even if no individual probe crosses the\n * suspicious threshold. Capped at MAX_BREADCRUMB_FLUSHES per mount.\n */\nexport const useConversationLayoutDiagnostics = ({\n conversationId,\n replyRootId,\n}: {\n conversationId: number\n replyRootId?: string | null\n}) => {\n const { width: windowWidth } = useWindowDimensions()\n const mountedAtRef = useRef(Date.now())\n const flushCountRef = useRef(0)\n\n useEffect(() => {\n if (Platform.OS !== 'android') return\n\n samplesByLabel.clear()\n reportedSuspiciousLabels.clear()\n mountedAtRef.current = Date.now()\n flushCountRef.current = 0\n\n const interval = setInterval(() => {\n if (flushCountRef.current >= MAX_BREADCRUMB_FLUSHES || samplesByLabel.size === 0) return\n\n flushCountRef.current += 1\n Log.reportError(new Error('CHAT-476: layout breadcrumb'), {\n scope: 'component',\n tags: { team: 'chat', chat476: 'breadcrumb' },\n extra: {\n conversationId,\n replyRootId,\n msSinceMount: Date.now() - mountedAtRef.current,\n windowWidth,\n pixelRatio: PixelRatio.get(),\n fontScale: PixelRatio.getFontScale(),\n samples: [...samplesByLabel.entries()].map(\n ([label, { width, height, x }]) => `${label}|w=${width}|h=${height}|x=${x}`\n ),\n },\n })\n }, BREADCRUMB_INTERVAL_MS)\n\n return () => clearInterval(interval)\n }, [conversationId, replyRootId, windowWidth])\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aspect_ratio.d.ts","sourceRoot":"","sources":["../../src/utils/aspect_ratio.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,GAC1B,OAAO,MAAM,GAAG,SAAS,EACzB,QAAQ,MAAM,GAAG,SAAS,EAC1B,iBAAY,KACX,MAKF,CAAA"}
|
|
@@ -1,11 +0,0 @@
|
|
|
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.
|
|
5
|
-
export const safeAspectRatio = (width, height, fallback = 1) => {
|
|
6
|
-
if (!width || !height)
|
|
7
|
-
return fallback;
|
|
8
|
-
const ratio = width / height;
|
|
9
|
-
return Number.isFinite(ratio) && ratio > 0 ? ratio : fallback;
|
|
10
|
-
};
|
|
11
|
-
//# sourceMappingURL=aspect_ratio.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aspect_ratio.js","sourceRoot":"","sources":["../../src/utils/aspect_ratio.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,sDAAsD;AACtD,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,KAAyB,EACzB,MAA0B,EAC1B,QAAQ,GAAG,CAAC,EACJ,EAAE;IACV,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAA;IAEtC,MAAM,KAAK,GAAG,KAAK,GAAG,MAAM,CAAA;IAC5B,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC/D,CAAC,CAAA","sourcesContent":["// assertKeysAreNumbers coerces unparseable attachment dimensions to 0, so a\n// raw width / height can be NaN or Infinity. Yoga ignores a NaN aspectRatio\n// and resolves Infinity by collapsing the dependent dimension to 0, so both\n// produce a silently broken box rather than an error.\nexport const safeAspectRatio = (\n width: number | undefined,\n height: number | undefined,\n fallback = 1\n): number => {\n if (!width || !height) return fallback\n\n const ratio = width / height\n return Number.isFinite(ratio) && ratio > 0 ? ratio : fallback\n}\n"]}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react-native'
|
|
2
|
-
import { LayoutChangeEvent, Platform } from 'react-native'
|
|
3
|
-
import {
|
|
4
|
-
__resetLayoutDiagnosticsForTests,
|
|
5
|
-
useConversationLayoutDiagnostics,
|
|
6
|
-
useLayoutWidthProbe,
|
|
7
|
-
} from '../../hooks/use_conversation_layout_diagnostics'
|
|
8
|
-
import { Log } from '../../utils'
|
|
9
|
-
|
|
10
|
-
const layoutEvent = ({
|
|
11
|
-
width,
|
|
12
|
-
height = 20,
|
|
13
|
-
x = 0,
|
|
14
|
-
y = 0,
|
|
15
|
-
}: {
|
|
16
|
-
width: number
|
|
17
|
-
height?: number
|
|
18
|
-
x?: number
|
|
19
|
-
y?: number
|
|
20
|
-
}) =>
|
|
21
|
-
({
|
|
22
|
-
nativeEvent: { layout: { width, height, x, y } },
|
|
23
|
-
}) as LayoutChangeEvent
|
|
24
|
-
|
|
25
|
-
describe('useLayoutWidthProbe', () => {
|
|
26
|
-
let reportError: jest.SpyInstance
|
|
27
|
-
let originalOS: typeof Platform.OS
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
reportError = jest.spyOn(Log, 'reportError').mockImplementation(() => {})
|
|
31
|
-
originalOS = Platform.OS
|
|
32
|
-
Platform.OS = 'android'
|
|
33
|
-
__resetLayoutDiagnosticsForTests()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
jest.restoreAllMocks()
|
|
38
|
-
Platform.OS = originalOS
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('returns undefined on non-Android platforms', () => {
|
|
42
|
-
Platform.OS = 'ios'
|
|
43
|
-
const { result } = renderHook(() => useLayoutWidthProbe('test.label', 16))
|
|
44
|
-
|
|
45
|
-
expect(result.current).toBeUndefined()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('reports once when width drops to at or below its inset', () => {
|
|
49
|
-
const { result } = renderHook(() => useLayoutWidthProbe('badge.metaLabel', 4))
|
|
50
|
-
|
|
51
|
-
result.current?.(layoutEvent({ width: 3 }))
|
|
52
|
-
|
|
53
|
-
expect(reportError).toHaveBeenCalledWith(
|
|
54
|
-
expect.objectContaining({
|
|
55
|
-
message: expect.stringContaining(
|
|
56
|
-
'badge.metaLabel measured at or below its horizontal inset'
|
|
57
|
-
),
|
|
58
|
-
}),
|
|
59
|
-
expect.objectContaining({
|
|
60
|
-
scope: 'component',
|
|
61
|
-
tags: expect.objectContaining({ chat476: 'near-zero-width' }),
|
|
62
|
-
extra: expect.objectContaining({ label: 'badge.metaLabel', width: 3, inset: 4 }),
|
|
63
|
-
})
|
|
64
|
-
)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('does not report when width stays comfortably above its inset', () => {
|
|
68
|
-
const { result } = renderHook(() => useLayoutWidthProbe('badge.metaLabel', 4))
|
|
69
|
-
|
|
70
|
-
result.current?.(layoutEvent({ width: 40 }))
|
|
71
|
-
|
|
72
|
-
expect(reportError).not.toHaveBeenCalled()
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('reports the same label only once across re-renders', () => {
|
|
76
|
-
const { result } = renderHook(() => useLayoutWidthProbe('form.textInput', 16))
|
|
77
|
-
|
|
78
|
-
result.current?.(layoutEvent({ width: 16 }))
|
|
79
|
-
result.current?.(layoutEvent({ width: 16 }))
|
|
80
|
-
result.current?.(layoutEvent({ width: 17 }))
|
|
81
|
-
|
|
82
|
-
expect(reportError).toHaveBeenCalledTimes(1)
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('useConversationLayoutDiagnostics', () => {
|
|
87
|
-
let reportError: jest.SpyInstance
|
|
88
|
-
let originalOS: typeof Platform.OS
|
|
89
|
-
|
|
90
|
-
beforeEach(() => {
|
|
91
|
-
jest.useFakeTimers()
|
|
92
|
-
reportError = jest.spyOn(Log, 'reportError').mockImplementation(() => {})
|
|
93
|
-
originalOS = Platform.OS
|
|
94
|
-
Platform.OS = 'android'
|
|
95
|
-
__resetLayoutDiagnosticsForTests()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
afterEach(() => {
|
|
99
|
-
jest.useRealTimers()
|
|
100
|
-
jest.restoreAllMocks()
|
|
101
|
-
Platform.OS = originalOS
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
test('does not flush a breadcrumb when no probe has reported a sample yet', () => {
|
|
105
|
-
renderHook(() => useConversationLayoutDiagnostics({ conversationId: 1069878 }))
|
|
106
|
-
|
|
107
|
-
jest.advanceTimersByTime(2000)
|
|
108
|
-
|
|
109
|
-
expect(reportError).not.toHaveBeenCalled()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('flushes a breadcrumb on an interval once a probe has a sample', () => {
|
|
113
|
-
const { result: probe } = renderHook(() => useLayoutWidthProbe('form.textInput', 16))
|
|
114
|
-
renderHook(() =>
|
|
115
|
-
useConversationLayoutDiagnostics({ conversationId: 1069878, replyRootId: null })
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
probe.current?.(layoutEvent({ width: 200 }))
|
|
119
|
-
jest.advanceTimersByTime(500)
|
|
120
|
-
|
|
121
|
-
expect(reportError).toHaveBeenCalledWith(
|
|
122
|
-
expect.objectContaining({ message: expect.stringContaining('layout breadcrumb') }),
|
|
123
|
-
expect.objectContaining({
|
|
124
|
-
tags: expect.objectContaining({ chat476: 'breadcrumb' }),
|
|
125
|
-
extra: expect.objectContaining({
|
|
126
|
-
conversationId: 1069878,
|
|
127
|
-
samples: expect.arrayContaining([expect.stringContaining('form.textInput|w=200')]),
|
|
128
|
-
}),
|
|
129
|
-
})
|
|
130
|
-
)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('caps the number of breadcrumb flushes per mount', () => {
|
|
134
|
-
const { result: probe } = renderHook(() => useLayoutWidthProbe('form.textInput', 16))
|
|
135
|
-
renderHook(() => useConversationLayoutDiagnostics({ conversationId: 1069878 }))
|
|
136
|
-
|
|
137
|
-
probe.current?.(layoutEvent({ width: 200 }))
|
|
138
|
-
jest.advanceTimersByTime(500 * 10)
|
|
139
|
-
|
|
140
|
-
expect(reportError).toHaveBeenCalledTimes(6)
|
|
141
|
-
})
|
|
142
|
-
})
|
|
@@ -1,123 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { safeAspectRatio } from '../aspect_ratio'
|
|
2
|
-
|
|
3
|
-
describe('safeAspectRatio', () => {
|
|
4
|
-
it.each([
|
|
5
|
-
[200, 100, 2],
|
|
6
|
-
[100, 200, 0.5],
|
|
7
|
-
[100, 100, 1],
|
|
8
|
-
])('divides width %p by height %p to get %p', (width, height, expected) => {
|
|
9
|
-
expect(safeAspectRatio(width, height)).toBe(expected)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
describe('with values that would produce a non-positive or non-finite ratio', () => {
|
|
13
|
-
it.each([
|
|
14
|
-
[0, 100],
|
|
15
|
-
[100, 0],
|
|
16
|
-
[0, 0],
|
|
17
|
-
[-100, 100],
|
|
18
|
-
[100, -100],
|
|
19
|
-
[undefined, 100],
|
|
20
|
-
[100, undefined],
|
|
21
|
-
[undefined, undefined],
|
|
22
|
-
[NaN, 100],
|
|
23
|
-
[100, NaN],
|
|
24
|
-
[Infinity, 100],
|
|
25
|
-
])('falls back to 1 for width %p, height %p', (width, height) => {
|
|
26
|
-
expect(safeAspectRatio(width, height)).toBe(1)
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('falls back to a caller-supplied value instead of the default', () => {
|
|
31
|
-
expect(safeAspectRatio(0, 100, 0.75)).toBe(0.75)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('never returns 0, a negative number, or a non-finite number', () => {
|
|
35
|
-
const invertedFallbackCases: Array<[number | undefined, number | undefined]> = [
|
|
36
|
-
[0, 100],
|
|
37
|
-
[100, 0],
|
|
38
|
-
[-100, 100],
|
|
39
|
-
[NaN, 100],
|
|
40
|
-
[Infinity, 100],
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
for (const [width, height] of invertedFallbackCases) {
|
|
44
|
-
const ratio = safeAspectRatio(width, height)
|
|
45
|
-
expect(ratio).not.toBe(0)
|
|
46
|
-
expect(Number.isFinite(ratio)).toBe(true)
|
|
47
|
-
expect(ratio).toBeGreaterThan(0)
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
})
|
|
@@ -1,14 +0,0 @@
|
|
|
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.
|
|
5
|
-
export const safeAspectRatio = (
|
|
6
|
-
width: number | undefined,
|
|
7
|
-
height: number | undefined,
|
|
8
|
-
fallback = 1
|
|
9
|
-
): number => {
|
|
10
|
-
if (!width || !height) return fallback
|
|
11
|
-
|
|
12
|
-
const ratio = width / height
|
|
13
|
-
return Number.isFinite(ratio) && ratio > 0 ? ratio : fallback
|
|
14
|
-
}
|