@planningcenter/chat-react-native 3.44.1-qa-874.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/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 +15 -14
- 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 +2 -1
- 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 +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 +10 -3
- 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 +16 -3
- 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/hooks/use_conversation_messages.d.ts +1 -0
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +10 -2
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +2 -3
- package/build/navigation/index.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +22 -5
- 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 +2 -0
- package/build/utils/aspect_ratio.d.ts.map +1 -0
- package/build/utils/aspect_ratio.js +11 -0
- package/build/utils/aspect_ratio.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -0
- package/build/utils/index.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/attachments/giphy_attachment.tsx +2 -2
- package/src/components/conversation/attachments/image_attachment.tsx +30 -26
- package/src/components/conversation/attachments/video_attachment.tsx +2 -1
- 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 +10 -3
- package/src/components/display/badge.tsx +20 -2
- package/src/hooks/use_conversation_layout_diagnostics.ts +123 -0
- package/src/hooks/use_conversation_messages.ts +10 -2
- package/src/navigation/index.tsx +2 -3
- package/src/screens/conversation_screen.tsx +25 -4
- package/src/screens/message_report_screen.tsx +6 -4
- package/src/utils/__tests__/aspect_ratio.test.ts +50 -0
- package/src/utils/aspect_ratio.ts +14 -0
- package/src/utils/index.ts +1 -0
|
@@ -3,6 +3,7 @@ import React, { useRef, useState } from 'react'
|
|
|
3
3
|
import { View, StyleSheet } from 'react-native'
|
|
4
4
|
import { useTheme } from '../../../hooks'
|
|
5
5
|
import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
|
|
6
|
+
import { safeAspectRatio } from '../../../utils'
|
|
6
7
|
import { Video, VideoPlayerHandle } from '../../../utils/native_adapters'
|
|
7
8
|
import { tokens } from '../../../vendor/tapestry/tokens'
|
|
8
9
|
import { Icon } from '../../display'
|
|
@@ -58,7 +59,7 @@ export function VideoAttachment({
|
|
|
58
59
|
<Video.Player
|
|
59
60
|
ref={videoRef}
|
|
60
61
|
source={{ uri: url }}
|
|
61
|
-
aspectRatio={width
|
|
62
|
+
aspectRatio={safeAspectRatio(width, height)}
|
|
62
63
|
style={styles.video}
|
|
63
64
|
onFullscreenPlayerWillDismiss={onFullscreenPlayerWillDismiss}
|
|
64
65
|
/>
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
useTheme,
|
|
18
18
|
} from '../../hooks'
|
|
19
19
|
import { useDeletingIds } from '../../hooks'
|
|
20
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
21
|
+
import { useLayoutWidthProbe } from '../../hooks/use_conversation_layout_diagnostics'
|
|
20
22
|
import { useConversationMessage } from '../../hooks/use_conversation_message'
|
|
21
23
|
import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
|
|
22
24
|
import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
|
|
@@ -69,6 +71,8 @@ export function Message({
|
|
|
69
71
|
const isPersisted = !isNewMessage(message)
|
|
70
72
|
const [temporarilyHidePendingState, setTemporarilyHidePendingState] = React.useState(true)
|
|
71
73
|
const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
|
|
74
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
75
|
+
const onMessageBubbleLayout = useLayoutWidthProbe('message.bubble', 0)
|
|
72
76
|
const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
|
|
73
77
|
useAnimatedMessageBackgroundColor()
|
|
74
78
|
const { message: replyRootMessage } = useConversationMessage({
|
|
@@ -231,7 +235,10 @@ export function Message({
|
|
|
231
235
|
)}
|
|
232
236
|
<View
|
|
233
237
|
style={styles.messageBubble}
|
|
234
|
-
onLayout={e =>
|
|
238
|
+
onLayout={e => {
|
|
239
|
+
setMessageBubbleHeight(e.nativeEvent.layout.height)
|
|
240
|
+
onMessageBubbleLayout?.(e)
|
|
241
|
+
}}
|
|
235
242
|
>
|
|
236
243
|
{isDeletedReplyRootMessage ? (
|
|
237
244
|
<View style={styles.messageText}>
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
|
|
25
25
|
import { useBroadcastTypingStatus } from '../../hooks/use_broadcast_typing_status'
|
|
26
26
|
import { useChatConfiguration } from '../../hooks/use_chat_configuration'
|
|
27
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
28
|
+
import { useLayoutWidthProbe } from '../../hooks/use_conversation_layout_diagnostics'
|
|
27
29
|
import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
|
|
28
30
|
import { useMessageDraft } from '../../hooks/use_message_draft'
|
|
29
31
|
import { ConversationScreenProps } from '../../screens/conversation_screen'
|
|
@@ -430,6 +432,11 @@ function MessageFormInput() {
|
|
|
430
432
|
|
|
431
433
|
const broadcastTypingStatus = useBroadcastTypingStatus()
|
|
432
434
|
|
|
435
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
436
|
+
const onTextInputBoundaryLayout = useLayoutWidthProbe('form.textInputBoundary', 2)
|
|
437
|
+
const onInputErrorMessageLayout = useLayoutWidthProbe('form.errorText', 32)
|
|
438
|
+
const onTextInputLayout = useLayoutWidthProbe('form.textInput', 32)
|
|
439
|
+
|
|
433
440
|
const handleTextChange = (newText: string) => {
|
|
434
441
|
setText(newText)
|
|
435
442
|
|
|
@@ -448,9 +455,13 @@ function MessageFormInput() {
|
|
|
448
455
|
}
|
|
449
456
|
|
|
450
457
|
return (
|
|
451
|
-
<View style={styles.textInputBoundary}>
|
|
458
|
+
<View style={styles.textInputBoundary} onLayout={onTextInputBoundaryLayout}>
|
|
452
459
|
<MessageFormAttachments />
|
|
453
|
-
{attachmentError ?
|
|
460
|
+
{attachmentError ? (
|
|
461
|
+
<Text style={styles.inputErrorMessage} onLayout={onInputErrorMessageLayout}>
|
|
462
|
+
{attachmentError}
|
|
463
|
+
</Text>
|
|
464
|
+
) : null}
|
|
454
465
|
<View style={styles.textInputRow}>
|
|
455
466
|
{usingGiphy ? (
|
|
456
467
|
<View style={styles.giphyBadge}>
|
|
@@ -472,6 +483,7 @@ function MessageFormInput() {
|
|
|
472
483
|
onSubmitEditing={onSubmit}
|
|
473
484
|
submitBehavior={usingGiphy ? 'submit' : 'newline'}
|
|
474
485
|
editable={inputEditable}
|
|
486
|
+
onLayout={onTextInputLayout}
|
|
475
487
|
/>
|
|
476
488
|
</View>
|
|
477
489
|
</View>
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
useScalableNumberOfLines,
|
|
10
10
|
useTheme,
|
|
11
11
|
} from '../../hooks'
|
|
12
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
13
|
+
import { useLayoutWidthProbe } from '../../hooks/use_conversation_layout_diagnostics'
|
|
12
14
|
import { useConversationMessage } from '../../hooks/use_conversation_message'
|
|
13
15
|
import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
|
|
14
16
|
import { MessageResource } from '../../types'
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
DenormalizedExpandedLinkAttachmentResource,
|
|
19
21
|
DenormalizedMessageAttachmentResource,
|
|
20
22
|
} from '../../types/resources/denormalized_attachment_resource'
|
|
21
|
-
import { assertKeysAreNumbers, pluralize } from '../../utils'
|
|
23
|
+
import { assertKeysAreNumbers, pluralize, safeAspectRatio } from '../../utils'
|
|
22
24
|
import {
|
|
23
25
|
CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
|
|
24
26
|
MAX_FONT_SIZE_MULTIPLIER,
|
|
@@ -70,6 +72,8 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
|
|
|
70
72
|
const timestamp = useLiveRelativeTime(message.createdAt)
|
|
71
73
|
|
|
72
74
|
const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
|
|
75
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
76
|
+
const onMessageBubbleLayout = useLayoutWidthProbe('replyShadow.bubble', 18)
|
|
73
77
|
const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
|
|
74
78
|
useAnimatedMessageBackgroundColor()
|
|
75
79
|
const scalableNumberOfLines = useScalableNumberOfLines(2)
|
|
@@ -119,7 +123,10 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
|
|
|
119
123
|
<View style={styles.messageContent}>
|
|
120
124
|
<View
|
|
121
125
|
style={styles.messageBubble}
|
|
122
|
-
onLayout={e =>
|
|
126
|
+
onLayout={e => {
|
|
127
|
+
setMessageBubbleHeight(e.nativeEvent.layout.height)
|
|
128
|
+
onMessageBubbleLayout?.(e)
|
|
129
|
+
}}
|
|
123
130
|
>
|
|
124
131
|
<MessageAttachmentImagery attachments={attachments} />
|
|
125
132
|
{text && (
|
|
@@ -324,7 +331,7 @@ const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}
|
|
|
324
331
|
},
|
|
325
332
|
imageWrapper: {
|
|
326
333
|
width: 32 * fontScale,
|
|
327
|
-
aspectRatio: imageWidth
|
|
334
|
+
aspectRatio: safeAspectRatio(imageWidth, imageHeight),
|
|
328
335
|
opacity: 0.5,
|
|
329
336
|
},
|
|
330
337
|
image: {
|
|
@@ -3,6 +3,8 @@ import type { TextStyle, ViewProps, ViewStyle } from 'react-native'
|
|
|
3
3
|
import { Pressable, StyleSheet, View } from 'react-native'
|
|
4
4
|
import Svg, { Defs, LinearGradient, Path, Stop } from 'react-native-svg'
|
|
5
5
|
import { useFontScale, useTheme } from '../../hooks'
|
|
6
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
7
|
+
import { useLayoutWidthProbe } from '../../hooks/use_conversation_layout_diagnostics'
|
|
6
8
|
import { platformFontWeightMedium } from '../../utils'
|
|
7
9
|
import { tokens } from '../../vendor/tapestry/tokens'
|
|
8
10
|
import { Icon, IconString } from './icon'
|
|
@@ -107,6 +109,14 @@ export function Badge({
|
|
|
107
109
|
}: BadgeProps) {
|
|
108
110
|
const styles = useStyles({ appearance, maxFontSizeMultiplier, variant })
|
|
109
111
|
const { showBadgeProductLogo } = useTheme()
|
|
112
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
113
|
+
// Inset is metaLabel's remaining contentInset now that its left offset is
|
|
114
|
+
// a margin, not padding - see the CHAT-476 candidate fix on this style.
|
|
115
|
+
const onLabelLayout = useLayoutWidthProbe('badge.label', 0)
|
|
116
|
+
const onMetaLabelLayout = useLayoutWidthProbe(
|
|
117
|
+
'badge.metaLabel',
|
|
118
|
+
styles.metaLabel.paddingHorizontal * 2
|
|
119
|
+
)
|
|
110
120
|
|
|
111
121
|
const isMetaSubtle = variant === 'metaSubtle'
|
|
112
122
|
const hasMetaLabel = Boolean(metaLabel)
|
|
@@ -127,6 +137,7 @@ export function Badge({
|
|
|
127
137
|
style={styles.label}
|
|
128
138
|
numberOfLines={1}
|
|
129
139
|
maxFontSizeMultiplier={maxFontSizeMultiplier}
|
|
140
|
+
onLayout={onLabelLayout}
|
|
130
141
|
>
|
|
131
142
|
{badgeLabel}
|
|
132
143
|
</Text>
|
|
@@ -138,6 +149,7 @@ export function Badge({
|
|
|
138
149
|
style={styles.metaLabel}
|
|
139
150
|
numberOfLines={numberOfLinesMetaLabel}
|
|
140
151
|
maxFontSizeMultiplier={maxFontSizeMultiplier}
|
|
152
|
+
onLayout={onMetaLabelLayout}
|
|
141
153
|
>
|
|
142
154
|
{metaLabel}
|
|
143
155
|
</Text>
|
|
@@ -234,7 +246,10 @@ const useStyles = ({
|
|
|
234
246
|
paddingVertical: variantStylesMap[variant].paddingVertical,
|
|
235
247
|
paddingHorizontal: variantStylesMap[variant].paddingHorizontal,
|
|
236
248
|
backgroundColor: variantStylesMap[variant].backgroundColor,
|
|
237
|
-
|
|
249
|
+
// metaSubtle's borderRadius is 0, so this would otherwise resolve to -2.
|
|
250
|
+
// Neither platform clamps a negative radius; Android's BackgroundDrawable
|
|
251
|
+
// reads it as "no rounded borders" and skips the rounded path entirely.
|
|
252
|
+
borderRadius: Math.max(0, variantStylesMap[variant].borderRadius - 2),
|
|
238
253
|
},
|
|
239
254
|
icon: {
|
|
240
255
|
color: statusColorMap[appearance].icon,
|
|
@@ -258,7 +273,10 @@ const useStyles = ({
|
|
|
258
273
|
fontSize: badgeFontSize,
|
|
259
274
|
alignSelf: 'center',
|
|
260
275
|
flexShrink: 1,
|
|
261
|
-
|
|
276
|
+
// CHAT-476 candidate fix: margin (not padding) so this offset sits
|
|
277
|
+
// outside contentInsets - a shrunk Text can never see it as part of
|
|
278
|
+
// its own measured content width, so it can't go negative from this.
|
|
279
|
+
marginLeft: variantStylesMap[variant].metaLabelPaddingLeft,
|
|
262
280
|
},
|
|
263
281
|
})
|
|
264
282
|
}
|
|
@@ -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
|
+
}
|
|
@@ -7,7 +7,7 @@ export const useConversationMessages = (
|
|
|
7
7
|
{ conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string | null },
|
|
8
8
|
opts?: SuspensePaginatorOptions
|
|
9
9
|
) => {
|
|
10
|
-
const { data, refetch, isRefetching, fetchNextPage, hasNextPage } =
|
|
10
|
+
const { data, refetch, isRefetching, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
|
11
11
|
useSuspensePaginator<MessageResource>(
|
|
12
12
|
getMessagesRequestArgs({ conversation_id, reply_root_id }),
|
|
13
13
|
opts
|
|
@@ -25,5 +25,13 @@ export const useConversationMessages = (
|
|
|
25
25
|
[data]
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
return {
|
|
28
|
+
return {
|
|
29
|
+
messages,
|
|
30
|
+
refetch,
|
|
31
|
+
isRefetching,
|
|
32
|
+
fetchNextPage,
|
|
33
|
+
hasNextPage,
|
|
34
|
+
isFetchingNextPage,
|
|
35
|
+
queryKey,
|
|
36
|
+
}
|
|
29
37
|
}
|
package/src/navigation/index.tsx
CHANGED
|
@@ -178,10 +178,9 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
178
178
|
composeOptions(
|
|
179
179
|
{
|
|
180
180
|
headerTitle: (route.params as { title?: string })?.title ?? 'Chat',
|
|
181
|
+
headerBackVisible: false,
|
|
181
182
|
},
|
|
182
|
-
|
|
183
|
-
onPress: navigation.goBack,
|
|
184
|
-
})
|
|
183
|
+
withRightClose({ onPress: navigation.goBack })
|
|
185
184
|
),
|
|
186
185
|
},
|
|
187
186
|
ConversationFilters: {
|
|
@@ -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'
|
|
@@ -91,12 +96,14 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
91
96
|
const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
|
|
92
97
|
route.params
|
|
93
98
|
const { data: conversation } = useConversation(route.params)
|
|
94
|
-
const { messages, fetchNextPage, hasNextPage } = useConversationMessages({
|
|
99
|
+
const { messages, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationMessages({
|
|
95
100
|
conversation_id,
|
|
96
101
|
reply_root_id,
|
|
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 })
|
|
@@ -216,7 +223,11 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
216
223
|
/>
|
|
217
224
|
)
|
|
218
225
|
}}
|
|
219
|
-
onEndReached={() =>
|
|
226
|
+
onEndReached={() => {
|
|
227
|
+
// VirtualizedList dedupes onEndReached per content length, not
|
|
228
|
+
// per fetch, so it can refire while a page is already in flight.
|
|
229
|
+
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
|
|
230
|
+
}}
|
|
220
231
|
ListHeaderComponent={<View style={styles.listHeader} />}
|
|
221
232
|
/>
|
|
222
233
|
)}
|
|
@@ -289,11 +300,13 @@ function InlineDateSeparator({ date }: DateSeparator) {
|
|
|
289
300
|
const { isThisYear } = getRelativeDateStatus(date)
|
|
290
301
|
const showYear = !isThisYear
|
|
291
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)
|
|
292
305
|
|
|
293
306
|
return (
|
|
294
307
|
<View style={styles.container}>
|
|
295
308
|
<View style={styles.separator} />
|
|
296
|
-
<Text variant="footnote" style={styles.dateText}>
|
|
309
|
+
<Text variant="footnote" style={styles.dateText} onLayout={onDateTextLayout}>
|
|
297
310
|
{dateStamp}
|
|
298
311
|
</Text>
|
|
299
312
|
<View style={styles.separator} />
|
|
@@ -342,11 +355,14 @@ export const ConversationScreenTitle = ({
|
|
|
342
355
|
const resourceType = badge?.pcoResourceType || ''
|
|
343
356
|
const productName = badge?.appName
|
|
344
357
|
const name = badge?.text || undefined
|
|
358
|
+
// CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
|
|
359
|
+
const onHeaderContainerLayout = useLayoutWidthProbe('header.container', 0)
|
|
345
360
|
|
|
346
361
|
return (
|
|
347
362
|
<PlatformPressable
|
|
348
363
|
accessibilityHint="Opens details about members and settings"
|
|
349
364
|
style={styles.container}
|
|
365
|
+
onLayout={onHeaderContainerLayout}
|
|
350
366
|
onPress={() => {
|
|
351
367
|
if (deleted) return
|
|
352
368
|
|
|
@@ -377,7 +393,12 @@ export const ConversationScreenTitle = ({
|
|
|
377
393
|
const usePressableHeaderStyle = () => {
|
|
378
394
|
const { width } = useWindowDimensions()
|
|
379
395
|
const slotReserve = useHeaderSlotReserve()
|
|
380
|
-
|
|
396
|
+
|
|
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.
|
|
401
|
+
const headerContainerStyle = { maxWidth: Math.max(0, width - slotReserve * 2) }
|
|
381
402
|
|
|
382
403
|
return StyleSheet.create({
|
|
383
404
|
container: {
|
|
@@ -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'
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|