@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.
Files changed (60) hide show
  1. package/build/components/conversation/attachments/giphy_attachment.js +2 -2
  2. package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
  3. package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
  4. package/build/components/conversation/attachments/image_attachment.js +14 -15
  5. package/build/components/conversation/attachments/image_attachment.js.map +1 -1
  6. package/build/components/conversation/attachments/video_attachment.d.ts.map +1 -1
  7. package/build/components/conversation/attachments/video_attachment.js +1 -2
  8. package/build/components/conversation/attachments/video_attachment.js.map +1 -1
  9. package/build/components/conversation/message.d.ts.map +1 -1
  10. package/build/components/conversation/message.js +4 -9
  11. package/build/components/conversation/message.js.map +1 -1
  12. package/build/components/conversation/message_form.d.ts.map +1 -1
  13. package/build/components/conversation/message_form.js +6 -10
  14. package/build/components/conversation/message_form.js.map +1 -1
  15. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  16. package/build/components/conversation/reply_shadow_message.js +6 -11
  17. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  18. package/build/components/display/badge.d.ts.map +1 -1
  19. package/build/components/display/badge.js +3 -16
  20. package/build/components/display/badge.js.map +1 -1
  21. package/build/hooks/use_conversation_messages.d.ts +0 -1
  22. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  23. package/build/hooks/use_conversation_messages.js +2 -10
  24. package/build/hooks/use_conversation_messages.js.map +1 -1
  25. package/build/screens/conversation_screen.d.ts.map +1 -1
  26. package/build/screens/conversation_screen.js +5 -22
  27. package/build/screens/conversation_screen.js.map +1 -1
  28. package/build/utils/index.d.ts +0 -1
  29. package/build/utils/index.d.ts.map +1 -1
  30. package/build/utils/index.js +0 -1
  31. package/build/utils/index.js.map +1 -1
  32. package/build/utils/message_text.d.ts +3 -0
  33. package/build/utils/message_text.d.ts.map +1 -0
  34. package/build/utils/message_text.js +8 -0
  35. package/build/utils/message_text.js.map +1 -0
  36. package/package.json +3 -3
  37. package/src/components/conversation/attachments/giphy_attachment.tsx +2 -2
  38. package/src/components/conversation/attachments/image_attachment.tsx +26 -30
  39. package/src/components/conversation/attachments/video_attachment.tsx +1 -2
  40. package/src/components/conversation/message.tsx +4 -9
  41. package/src/components/conversation/message_form.tsx +8 -17
  42. package/src/components/conversation/reply_shadow_message.tsx +6 -11
  43. package/src/components/display/badge.tsx +2 -20
  44. package/src/hooks/use_conversation_messages.ts +2 -10
  45. package/src/screens/conversation_screen.tsx +4 -25
  46. package/src/utils/__tests__/message_text.test.ts +45 -0
  47. package/src/utils/index.ts +0 -1
  48. package/src/utils/message_text.ts +9 -0
  49. package/build/hooks/use_conversation_layout_diagnostics.d.ts +0 -21
  50. package/build/hooks/use_conversation_layout_diagnostics.d.ts.map +0 -1
  51. package/build/hooks/use_conversation_layout_diagnostics.js +0 -99
  52. package/build/hooks/use_conversation_layout_diagnostics.js.map +0 -1
  53. package/build/utils/aspect_ratio.d.ts +0 -2
  54. package/build/utils/aspect_ratio.d.ts.map +0 -1
  55. package/build/utils/aspect_ratio.js +0 -11
  56. package/build/utils/aspect_ratio.js.map +0 -1
  57. package/src/__tests__/hooks/use_conversation_layout_diagnostics.test.tsx +0 -142
  58. package/src/hooks/use_conversation_layout_diagnostics.ts +0 -123
  59. package/src/utils/__tests__/aspect_ratio.test.ts +0 -50
  60. package/src/utils/aspect_ratio.ts +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.44.1-qa-876.0",
3
+ "version": "3.44.1-qa-staging.1",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "react-native": "./src/index.tsx",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@fortawesome/fontawesome-svg-core": "^7.2.0",
28
28
  "@fortawesome/react-native-fontawesome": "^0.3.2",
29
- "@planningcenter/emoji-keyboard": "3.44.1-qa-876.0",
29
+ "@planningcenter/emoji-keyboard": "3.44.1-qa-staging.1",
30
30
  "lodash-inflection": "^1.5.0",
31
31
  "react-compiler-runtime": "^1.0.0"
32
32
  },
@@ -73,5 +73,5 @@
73
73
  "sf-symbols-typescript": "^2.2.0",
74
74
  "typescript": "~6.0.3"
75
75
  },
76
- "gitHead": "041126159a2f3cb4d4c07139a06f3d301d1dabdd"
76
+ "gitHead": "57de03cd8388934cfe0cb792621309b5b02d5ab4"
77
77
  }
@@ -3,7 +3,7 @@ import React from 'react'
3
3
  import { Text, StyleSheet, Linking } from 'react-native'
4
4
  import { useTheme } from '../../../hooks'
5
5
  import { DenormalizedGiphyAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
6
- import { assertKeysAreNumbers, safeAspectRatio } from '../../../utils'
6
+ import { assertKeysAreNumbers } from '../../../utils'
7
7
  import { tokens } from '../../../vendor/tapestry/tokens'
8
8
  import { Image } from '../../display'
9
9
 
@@ -55,7 +55,7 @@ const useStyles = ({ imageWidth, imageHeight }: { imageWidth: number; imageHeigh
55
55
  },
56
56
  imageWrapper: {
57
57
  width: '100%',
58
- aspectRatio: safeAspectRatio(imageWidth, imageHeight),
58
+ aspectRatio: imageWidth / imageHeight,
59
59
  },
60
60
  image: {
61
61
  borderRadius: 8,
@@ -7,9 +7,9 @@ import {
7
7
  Modal,
8
8
  View,
9
9
  Linking,
10
+ Dimensions,
10
11
  NativeSyntheticEvent,
11
12
  NativeScrollEvent,
12
- useWindowDimensions,
13
13
  ViewToken,
14
14
  } from 'react-native'
15
15
  import {
@@ -30,12 +30,13 @@ import Animated, {
30
30
  import { useSafeAreaInsets, SafeAreaProvider } from 'react-native-safe-area-context'
31
31
  import { useTheme } from '../../../hooks'
32
32
  import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
33
- import { Haptic, platformFontWeightMedium, safeAspectRatio } from '../../../utils'
33
+ import { Haptic, platformFontWeightMedium } from '../../../utils'
34
34
  import { formatDatePreview } from '../../../utils/date'
35
35
  import { tokens } from '../../../vendor/tapestry/tokens'
36
36
  import { IconButton, Image, Heading, Text } from '../../display'
37
37
  import { AttachmentDeletingOverlay } from './attachment_deleting_overlay'
38
38
 
39
+ const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window')
39
40
  const DISMISS_PAN_THRESHOLD = 175
40
41
  const MIN_DISTANCE_FOR_PAN = 10 // Higher threshold gives pinching priority
41
42
  const SINGLE_FINGER_POINTER = 1 // Single-finger panning / tapping helps to avoid conflicts with pinching
@@ -166,7 +167,6 @@ const LightboxGallery = ({
166
167
  }: LightboxGalleryProps) => {
167
168
  const styles = useStyles()
168
169
  const insets = useSafeAreaInsets()
169
- const { width: windowWidth, height: windowHeight } = useWindowDimensions()
170
170
  const [currentImageIndex, setCurrentImageIndex] = useState(initialImageIndex)
171
171
  const { soft: triggerMaxZoomHaptic, rigid: triggerDismissHaptic } = Haptic
172
172
 
@@ -177,8 +177,8 @@ const LightboxGallery = ({
177
177
  const imageHeight = metadata.height
178
178
 
179
179
  // Calculate available space for image display
180
- const availableWindowWidth = windowWidth
181
- const availableWindowHeight = windowHeight - insets.top - insets.bottom
180
+ const availableWindowWidth = WINDOW_WIDTH
181
+ const availableWindowHeight = WINDOW_HEIGHT - insets.top - insets.bottom
182
182
 
183
183
  /* ============================
184
184
  ANIMATION VALUES
@@ -299,8 +299,8 @@ const LightboxGallery = ({
299
299
  'worklet'
300
300
 
301
301
  // How far the focal point is from the center of the window
302
- const windowCenterX = windowWidth / 2
303
- const windowCenterY = windowHeight / 2
302
+ const windowCenterX = WINDOW_WIDTH / 2
303
+ const windowCenterY = WINDOW_HEIGHT / 2
304
304
 
305
305
  // Position of focal point relative to current image position
306
306
  const focalRelativeX = focalPointX - windowCenterX - currentSavedTranslateX
@@ -402,32 +402,29 @@ const LightboxGallery = ({
402
402
  // Used in tandem with FlatList's initialScrollIndex to quickly calculate the position and size of each image before they load.
403
403
  const getItemLayout = useCallback(
404
404
  (_: unknown, index: number) => ({
405
- length: windowWidth,
406
- offset: windowWidth * index,
405
+ length: WINDOW_WIDTH,
406
+ offset: WINDOW_WIDTH * index,
407
407
  index,
408
408
  }),
409
- [windowWidth]
409
+ []
410
410
  )
411
411
 
412
412
  // Captures the current image's index after the FlatList finishes its scroll animation.
413
413
  // Used in tandem with onViewableItemsChanged to ensure the final value for currentImageIndex is set.
414
- const onMomentumScrollEnd = useCallback(
415
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
416
- // Calculate the index of the image that is currently visible
417
- const imageOffsetX = event.nativeEvent.contentOffset.x
418
- const newImageIndex = Math.round(imageOffsetX / windowWidth)
419
-
420
- // Check if the image index has changed and the FlatList didn't scroll past the first or last image
421
- const didImageIndexChange = newImageIndex !== stableCurrentImageIndexRef.current
422
- const isImageIndexWithinBounds =
423
- newImageIndex >= 0 && newImageIndex < stableImageAttachmentsLengthRef.current
424
-
425
- if (didImageIndexChange && isImageIndexWithinBounds) {
426
- setCurrentImageIndex(newImageIndex)
427
- }
428
- },
429
- [windowWidth]
430
- )
414
+ const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
415
+ // Calculate the index of the image that is currently visible
416
+ const imageOffsetX = event.nativeEvent.contentOffset.x
417
+ const newImageIndex = Math.round(imageOffsetX / WINDOW_WIDTH)
418
+
419
+ // Check if the image index has changed and the FlatList didn't scroll past the first or last image
420
+ const didImageIndexChange = newImageIndex !== stableCurrentImageIndexRef.current
421
+ const isImageIndexWithinBounds =
422
+ newImageIndex >= 0 && newImageIndex < stableImageAttachmentsLengthRef.current
423
+
424
+ if (didImageIndexChange && isImageIndexWithinBounds) {
425
+ setCurrentImageIndex(newImageIndex)
426
+ }
427
+ }, [])
431
428
 
432
429
  // Supplements onMomentumScrollEnd by capturing the current image's index while the FlatList is actively scrolling.
433
430
  // Used in tandem with viewabilityConfig to trigger when the image is 50% visible in the window.
@@ -932,7 +929,6 @@ interface UseStylesProps {
932
929
 
933
930
  const useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {}) => {
934
931
  const { top, bottom } = useSafeAreaInsets()
935
- const { width: windowWidth } = useWindowDimensions()
936
932
  const backgroundColor = tokens.colorNeutral7
937
933
  const transparentBackgroundColor = useMemo(
938
934
  () => colorFunction(backgroundColor).alpha(0.8).toString(),
@@ -950,7 +946,7 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {})
950
946
  attachmentImageWrapper: {
951
947
  width: '100%',
952
948
  minWidth: 200,
953
- aspectRatio: safeAspectRatio(imageWidth, imageHeight),
949
+ aspectRatio: imageWidth / imageHeight,
954
950
  },
955
951
  image: {
956
952
  borderRadius: 8,
@@ -964,7 +960,7 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 }: UseStylesProps = {})
964
960
  },
965
961
  gestureImage: {
966
962
  height: '100%',
967
- width: windowWidth,
963
+ width: WINDOW_WIDTH,
968
964
  backgroundColor: 'transparent',
969
965
  },
970
966
  gestureImageLoading: {
@@ -3,7 +3,6 @@ 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'
7
6
  import { Video, VideoPlayerHandle } from '../../../utils/native_adapters'
8
7
  import { tokens } from '../../../vendor/tapestry/tokens'
9
8
  import { Icon } from '../../display'
@@ -59,7 +58,7 @@ export function VideoAttachment({
59
58
  <Video.Player
60
59
  ref={videoRef}
61
60
  source={{ uri: url }}
62
- aspectRatio={safeAspectRatio(width, height)}
61
+ aspectRatio={width / height}
63
62
  style={styles.video}
64
63
  onFullscreenPlayerWillDismiss={onFullscreenPlayerWillDismiss}
65
64
  />
@@ -17,8 +17,6 @@ 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'
22
20
  import { useConversationMessage } from '../../hooks/use_conversation_message'
23
21
  import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
24
22
  import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
@@ -27,6 +25,7 @@ import { DenormalizedMessageAttachmentResource } from '../../types/resources/den
27
25
  import { ReactionCountResource } from '../../types/resources/reaction'
28
26
  import { pluralize } from '../../utils'
29
27
  import { isNewMessage } from '../../utils/cache/messages_cache'
28
+ import { stripEmptyTextCharacters } from '../../utils/message_text'
30
29
  import { Haptic } from '../../utils/native_adapters'
31
30
  import {
32
31
  CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
@@ -56,7 +55,8 @@ export function Message({
56
55
  inReplyScreen,
57
56
  ...message
58
57
  }: MessageProps) {
59
- const { text, reactionCounts, pending, error, attachments, author } = message
58
+ const { reactionCounts, pending, error, attachments, author } = message
59
+ const text = stripEmptyTextCharacters(message.text ?? '')
60
60
  const deletingMessageIds = useDeletingIds('deleteMessage')
61
61
  const isDeleting = deletingMessageIds.has(message.id)
62
62
  const styles = useMessageStyles(message.mine, isDeleting)
@@ -71,8 +71,6 @@ export function Message({
71
71
  const isPersisted = !isNewMessage(message)
72
72
  const [temporarilyHidePendingState, setTemporarilyHidePendingState] = React.useState(true)
73
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)
76
74
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
77
75
  useAnimatedMessageBackgroundColor()
78
76
  const { message: replyRootMessage } = useConversationMessage({
@@ -235,10 +233,7 @@ export function Message({
235
233
  )}
236
234
  <View
237
235
  style={styles.messageBubble}
238
- onLayout={e => {
239
- setMessageBubbleHeight(e.nativeEvent.layout.height)
240
- onMessageBubbleLayout?.(e)
241
- }}
236
+ onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
242
237
  >
243
238
  {isDeletedReplyRootMessage ? (
244
239
  <View style={styles.messageText}>
@@ -24,8 +24,6 @@ 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'
29
27
  import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
30
28
  import { useMessageDraft } from '../../hooks/use_message_draft'
31
29
  import { ConversationScreenProps } from '../../screens/conversation_screen'
@@ -43,6 +41,7 @@ import {
43
41
  import { pickAttachmentPreviewKind } from '../../utils/attachment_kind'
44
42
  import { canSubmitMessage } from '../../utils/can_submit_message'
45
43
  import { isInputEditable } from '../../utils/is_input_editable'
44
+ import { hasMessageText, stripEmptyTextCharacters } from '../../utils/message_text'
46
45
  import {
47
46
  DocumentPicker,
48
47
  DocumentPickerResult,
@@ -195,7 +194,7 @@ function MessageFormRoot({
195
194
  conversationDisabled: conversation.disabled ?? false,
196
195
  pendingUploads: !!attachmentUploader?.pendingUploads,
197
196
  hasFlaggedAttachments: !!attachmentUploader?.flaggedAttachmentCount,
198
- hasText: text.trim().length > 0,
197
+ hasText: hasMessageText(text),
199
198
  hasSendableAttachments: !!attachmentUploader?.attachmentIds?.length,
200
199
  })
201
200
  const disabled = !canSubmit
@@ -243,11 +242,13 @@ function MessageFormRoot({
243
242
  file: attachment.file,
244
243
  })
245
244
  )
245
+ const messageText = stripEmptyTextCharacters(text)
246
+
246
247
  if (currentlyEditingMessage) {
247
- mutateAsync({ text })
248
+ mutateAsync({ text: messageText })
248
249
  reset()
249
250
  } else {
250
- mutateAsync({ text, attachments: attachmentsForSubmit })
251
+ mutateAsync({ text: messageText, attachments: attachmentsForSubmit })
251
252
  reset()
252
253
  }
253
254
  }
@@ -432,11 +433,6 @@ function MessageFormInput() {
432
433
 
433
434
  const broadcastTypingStatus = useBroadcastTypingStatus()
434
435
 
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
-
440
436
  const handleTextChange = (newText: string) => {
441
437
  setText(newText)
442
438
 
@@ -455,13 +451,9 @@ function MessageFormInput() {
455
451
  }
456
452
 
457
453
  return (
458
- <View style={styles.textInputBoundary} onLayout={onTextInputBoundaryLayout}>
454
+ <View style={styles.textInputBoundary}>
459
455
  <MessageFormAttachments />
460
- {attachmentError ? (
461
- <Text style={styles.inputErrorMessage} onLayout={onInputErrorMessageLayout}>
462
- {attachmentError}
463
- </Text>
464
- ) : null}
456
+ {attachmentError ? <Text style={styles.inputErrorMessage}>{attachmentError}</Text> : null}
465
457
  <View style={styles.textInputRow}>
466
458
  {usingGiphy ? (
467
459
  <View style={styles.giphyBadge}>
@@ -483,7 +475,6 @@ function MessageFormInput() {
483
475
  onSubmitEditing={onSubmit}
484
476
  submitBehavior={usingGiphy ? 'submit' : 'newline'}
485
477
  editable={inputEditable}
486
- onLayout={onTextInputLayout}
487
478
  />
488
479
  </View>
489
480
  </View>
@@ -9,8 +9,6 @@ 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'
14
12
  import { useConversationMessage } from '../../hooks/use_conversation_message'
15
13
  import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
16
14
  import { MessageResource } from '../../types'
@@ -20,7 +18,8 @@ import {
20
18
  DenormalizedExpandedLinkAttachmentResource,
21
19
  DenormalizedMessageAttachmentResource,
22
20
  } from '../../types/resources/denormalized_attachment_resource'
23
- import { assertKeysAreNumbers, pluralize, safeAspectRatio } from '../../utils'
21
+ import { assertKeysAreNumbers, pluralize } from '../../utils'
22
+ import { stripEmptyTextCharacters } from '../../utils/message_text'
24
23
  import {
25
24
  CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
26
25
  MAX_FONT_SIZE_MULTIPLIER,
@@ -65,15 +64,14 @@ interface ShadowMessageContentProps extends MessageResource {
65
64
  }
66
65
 
67
66
  function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageContentProps) {
68
- const { text, deletedAt, author, attachments } = message
67
+ const { deletedAt, author, attachments } = message
68
+ const text = stripEmptyTextCharacters(message.text ?? '')
69
69
  const styles = useStyles(message)
70
70
  const { colors } = useTheme()
71
71
  const navigation = useNavigation()
72
72
  const timestamp = useLiveRelativeTime(message.createdAt)
73
73
 
74
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)
77
75
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
78
76
  useAnimatedMessageBackgroundColor()
79
77
  const scalableNumberOfLines = useScalableNumberOfLines(2)
@@ -123,10 +121,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
123
121
  <View style={styles.messageContent}>
124
122
  <View
125
123
  style={styles.messageBubble}
126
- onLayout={e => {
127
- setMessageBubbleHeight(e.nativeEvent.layout.height)
128
- onMessageBubbleLayout?.(e)
129
- }}
124
+ onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
130
125
  >
131
126
  <MessageAttachmentImagery attachments={attachments} />
132
127
  {text && (
@@ -331,7 +326,7 @@ const useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}
331
326
  },
332
327
  imageWrapper: {
333
328
  width: 32 * fontScale,
334
- aspectRatio: safeAspectRatio(imageWidth, imageHeight),
329
+ aspectRatio: imageWidth / imageHeight,
335
330
  opacity: 0.5,
336
331
  },
337
332
  image: {
@@ -3,8 +3,6 @@ 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'
8
6
  import { platformFontWeightMedium } from '../../utils'
9
7
  import { tokens } from '../../vendor/tapestry/tokens'
10
8
  import { Icon, IconString } from './icon'
@@ -109,14 +107,6 @@ export function Badge({
109
107
  }: BadgeProps) {
110
108
  const styles = useStyles({ appearance, maxFontSizeMultiplier, variant })
111
109
  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
- )
120
110
 
121
111
  const isMetaSubtle = variant === 'metaSubtle'
122
112
  const hasMetaLabel = Boolean(metaLabel)
@@ -137,7 +127,6 @@ export function Badge({
137
127
  style={styles.label}
138
128
  numberOfLines={1}
139
129
  maxFontSizeMultiplier={maxFontSizeMultiplier}
140
- onLayout={onLabelLayout}
141
130
  >
142
131
  {badgeLabel}
143
132
  </Text>
@@ -149,7 +138,6 @@ export function Badge({
149
138
  style={styles.metaLabel}
150
139
  numberOfLines={numberOfLinesMetaLabel}
151
140
  maxFontSizeMultiplier={maxFontSizeMultiplier}
152
- onLayout={onMetaLabelLayout}
153
141
  >
154
142
  {metaLabel}
155
143
  </Text>
@@ -246,10 +234,7 @@ const useStyles = ({
246
234
  paddingVertical: variantStylesMap[variant].paddingVertical,
247
235
  paddingHorizontal: variantStylesMap[variant].paddingHorizontal,
248
236
  backgroundColor: variantStylesMap[variant].backgroundColor,
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),
237
+ borderRadius: variantStylesMap[variant].borderRadius - 2,
253
238
  },
254
239
  icon: {
255
240
  color: statusColorMap[appearance].icon,
@@ -273,10 +258,7 @@ const useStyles = ({
273
258
  fontSize: badgeFontSize,
274
259
  alignSelf: 'center',
275
260
  flexShrink: 1,
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,
261
+ paddingLeft: variantStylesMap[variant].metaLabelPaddingLeft,
280
262
  },
281
263
  })
282
264
  }
@@ -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, isFetchingNextPage } =
10
+ const { data, refetch, isRefetching, fetchNextPage, hasNextPage } =
11
11
  useSuspensePaginator<MessageResource>(
12
12
  getMessagesRequestArgs({ conversation_id, reply_root_id }),
13
13
  opts
@@ -25,13 +25,5 @@ export const useConversationMessages = (
25
25
  [data]
26
26
  )
27
27
 
28
- return {
29
- messages,
30
- refetch,
31
- isRefetching,
32
- fetchNextPage,
33
- hasNextPage,
34
- isFetchingNextPage,
35
- queryKey,
36
- }
28
+ return { messages, refetch, isRefetching, fetchNextPage, hasNextPage, queryKey }
37
29
  }
@@ -32,11 +32,6 @@ 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'
40
35
  import { useConversationMessages } from '../hooks/use_conversation_messages'
41
36
  import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
42
37
  import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
@@ -96,14 +91,12 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
96
91
  const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
97
92
  route.params
98
93
  const { data: conversation } = useConversation(route.params)
99
- const { messages, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationMessages({
94
+ const { messages, fetchNextPage, hasNextPage } = useConversationMessages({
100
95
  conversation_id,
101
96
  reply_root_id,
102
97
  })
103
98
  useConversationJoltEvents({ conversationId: conversation_id })
104
99
  useConversationMessagesJoltEvents({ conversationId: conversation_id })
105
- // CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
106
- useConversationLayoutDiagnostics({ conversationId: conversation_id, replyRootId: reply_root_id })
107
100
  const { markHandledByMessageForm } = useVideoModerationJoltEvents()
108
101
  useEnsureConversationsRouteExists()
109
102
  useMarkLatestMessageRead({ conversation, messages })
@@ -223,11 +216,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
223
216
  />
224
217
  )
225
218
  }}
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
- }}
219
+ onEndReached={() => fetchNextPage()}
231
220
  ListHeaderComponent={<View style={styles.listHeader} />}
232
221
  />
233
222
  )}
@@ -300,13 +289,11 @@ function InlineDateSeparator({ date }: DateSeparator) {
300
289
  const { isThisYear } = getRelativeDateStatus(date)
301
290
  const showYear = !isThisYear
302
291
  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)
305
292
 
306
293
  return (
307
294
  <View style={styles.container}>
308
295
  <View style={styles.separator} />
309
- <Text variant="footnote" style={styles.dateText} onLayout={onDateTextLayout}>
296
+ <Text variant="footnote" style={styles.dateText}>
310
297
  {dateStamp}
311
298
  </Text>
312
299
  <View style={styles.separator} />
@@ -355,14 +342,11 @@ export const ConversationScreenTitle = ({
355
342
  const resourceType = badge?.pcoResourceType || ''
356
343
  const productName = badge?.appName
357
344
  const name = badge?.text || undefined
358
- // CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
359
- const onHeaderContainerLayout = useLayoutWidthProbe('header.container', 0)
360
345
 
361
346
  return (
362
347
  <PlatformPressable
363
348
  accessibilityHint="Opens details about members and settings"
364
349
  style={styles.container}
365
- onLayout={onHeaderContainerLayout}
366
350
  onPress={() => {
367
351
  if (deleted) return
368
352
 
@@ -393,12 +377,7 @@ export const ConversationScreenTitle = ({
393
377
  const usePressableHeaderStyle = () => {
394
378
  const { width } = useWindowDimensions()
395
379
  const slotReserve = useHeaderSlotReserve()
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) }
380
+ const headerContainerStyle = { maxWidth: width - slotReserve * 2 }
402
381
 
403
382
  return StyleSheet.create({
404
383
  container: {
@@ -0,0 +1,45 @@
1
+ import { hasMessageText, stripEmptyTextCharacters } from '../message_text'
2
+
3
+ const EMPTY = '\uFFFC'
4
+
5
+ describe('stripEmptyTextCharacters', () => {
6
+ it.each([
7
+ ['leaves ordinary text alone', 'Come on over after.', 'Come on over after.'],
8
+ [
9
+ 'removes a trailing pair',
10
+ `Yes. Come on over after.${EMPTY}${EMPTY}`,
11
+ 'Yes. Come on over after.',
12
+ ],
13
+ ['removes one mid-string', `before${EMPTY}after`, 'beforeafter'],
14
+ ['reduces an all-empty body to an empty string', EMPTY, ''],
15
+ ['reduces a repeated all-empty body to an empty string', `${EMPTY}${EMPTY}${EMPTY}`, ''],
16
+ ['leaves an empty string alone', '', ''],
17
+ ])('%s', (_name, input, expected) => {
18
+ expect(stripEmptyTextCharacters(input)).toBe(expected)
19
+ })
20
+
21
+ it('does not disturb emoji or other multi-byte text', () => {
22
+ expect(stripEmptyTextCharacters(`👋 hi${EMPTY}`)).toBe('👋 hi')
23
+ })
24
+ })
25
+
26
+ describe('hasMessageText', () => {
27
+ it.each([
28
+ ['ordinary text', 'hello', true],
29
+ ['text alongside an empty character', `hello${EMPTY}`, true],
30
+ ['only an empty character', EMPTY, false],
31
+ ['several empty characters', `${EMPTY}${EMPTY}`, false],
32
+ ['an empty character surrounded by whitespace', ` ${EMPTY} `, false],
33
+ ['whitespace only', ' ', false],
34
+ ['empty string', '', false],
35
+ ['undefined', undefined, false],
36
+ ['null', null, false],
37
+ ])('%s -> %s', (_name, input, expected) => {
38
+ expect(hasMessageText(input)).toBe(expected)
39
+ })
40
+
41
+ it('is not fooled by trim alone, which does not treat the character as whitespace', () => {
42
+ expect(EMPTY.trim()).toBe(EMPTY)
43
+ expect(hasMessageText(EMPTY)).toBe(false)
44
+ })
45
+ })
@@ -1,5 +1,4 @@
1
1
  export * from './assert_keys_are_numbers'
2
- export * from './aspect_ratio'
3
2
  export * from './attachment_kind'
4
3
  export * from './chat_type_label'
5
4
  export * from './cache'
@@ -0,0 +1,9 @@
1
+ const EMPTY_TEXT_CHARACTERS = /\uFFFC/g
2
+
3
+ export function stripEmptyTextCharacters(text: string) {
4
+ return text.replace(EMPTY_TEXT_CHARACTERS, '')
5
+ }
6
+
7
+ export function hasMessageText(text?: string | null) {
8
+ return Boolean(text && stripEmptyTextCharacters(text).trim())
9
+ }
@@ -1,21 +0,0 @@
1
- import { LayoutChangeEvent } from 'react-native';
2
- export declare const __resetLayoutDiagnosticsForTests: () => void;
3
- /**
4
- * Attaches to a suspect Text/TextInput/View's onLayout and reports through
5
- * Log.reportError the first time its resolved width drops to at or below
6
- * its own horizontal inset (padding/border) — the shape that Android's
7
- * unclamped text-measure path can turn into a negative width. Returns
8
- * undefined on non-Android platforms so it's a no-op there.
9
- */
10
- export declare const useLayoutWidthProbe: (label: string, inset?: number) => ((event: LayoutChangeEvent) => void) | undefined;
11
- /**
12
- * Mount once per conversation screen. Periodically flushes every probed
13
- * label's latest known width as a single breadcrumb, so there's a timeline
14
- * leading up to the crash even if no individual probe crosses the
15
- * suspicious threshold. Capped at MAX_BREADCRUMB_FLUSHES per mount.
16
- */
17
- export declare const useConversationLayoutDiagnostics: ({ conversationId, replyRootId, }: {
18
- conversationId: number;
19
- replyRootId?: string | null;
20
- }) => void;
21
- //# sourceMappingURL=use_conversation_layout_diagnostics.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"use_conversation_layout_diagnostics.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_layout_diagnostics.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAA6C,MAAM,cAAc,CAAA;AAsB3F,eAAO,MAAM,gCAAgC,YAG5C,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,EAAE,cAAS,cAIhD,iBAAiB,sBAiC5B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,kCAG9C;IACD,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B,SAoCA,CAAA"}