@planningcenter/chat-react-native 3.44.1-qa-876.0 → 3.44.1-qa-staging.2

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 +25 -24
  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 +30 -26
  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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,kCAAkC,CAAA;AAChD,cAAc,mCAAmC,CAAA;AACjD,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,iCAAiC,CAAA;AAC/C,cAAc,sBAAsB,CAAA;AACpC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,mBAAmB,CAAA;AACjC,cAAc,SAAS,CAAA;AACvB,cAAc,OAAO,CAAA","sourcesContent":["export * from './assert_keys_are_numbers'\nexport * from './aspect_ratio'\nexport * from './attachment_kind'\nexport * from './chat_type_label'\nexport * from './cache'\nexport * from './client'\nexport * from './convert_attachments_for_create'\nexport * from './destructure_chat_group_graph_id'\nexport * from './host_bridge'\nexport * from './liquid_glass'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './conversation_safety_constants'\nexport * from './reaction_constants'\nexport * from './session'\nexport * from './styles'\nexport * from './system_messages'\nexport * from './theme'\nexport * from './uri'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,kCAAkC,CAAA;AAChD,cAAc,mCAAmC,CAAA;AACjD,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,iCAAiC,CAAA;AAC/C,cAAc,sBAAsB,CAAA;AACpC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,mBAAmB,CAAA;AACjC,cAAc,SAAS,CAAA;AACvB,cAAc,OAAO,CAAA","sourcesContent":["export * from './assert_keys_are_numbers'\nexport * from './attachment_kind'\nexport * from './chat_type_label'\nexport * from './cache'\nexport * from './client'\nexport * from './convert_attachments_for_create'\nexport * from './destructure_chat_group_graph_id'\nexport * from './host_bridge'\nexport * from './liquid_glass'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './conversation_safety_constants'\nexport * from './reaction_constants'\nexport * from './session'\nexport * from './styles'\nexport * from './system_messages'\nexport * from './theme'\nexport * from './uri'\n"]}
@@ -0,0 +1,3 @@
1
+ export declare function stripEmptyTextCharacters(text: string): string;
2
+ export declare function hasMessageText(text?: string | null): boolean;
3
+ //# sourceMappingURL=message_text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message_text.d.ts","sourceRoot":"","sources":["../../src/utils/message_text.ts"],"names":[],"mappings":"AAEA,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,UAEpD;AAED,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,WAElD"}
@@ -0,0 +1,8 @@
1
+ const EMPTY_TEXT_CHARACTERS = /\uFFFC/g;
2
+ export function stripEmptyTextCharacters(text) {
3
+ return text.replace(EMPTY_TEXT_CHARACTERS, '');
4
+ }
5
+ export function hasMessageText(text) {
6
+ return Boolean(text && stripEmptyTextCharacters(text).trim());
7
+ }
8
+ //# sourceMappingURL=message_text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message_text.js","sourceRoot":"","sources":["../../src/utils/message_text.ts"],"names":[],"mappings":"AAAA,MAAM,qBAAqB,GAAG,SAAS,CAAA;AAEvC,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAA;AAChD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAoB;IACjD,OAAO,OAAO,CAAC,IAAI,IAAI,wBAAwB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["const EMPTY_TEXT_CHARACTERS = /\\uFFFC/g\n\nexport function stripEmptyTextCharacters(text: string) {\n return text.replace(EMPTY_TEXT_CHARACTERS, '')\n}\n\nexport function hasMessageText(text?: string | null) {\n return Boolean(text && stripEmptyTextCharacters(text).trim())\n}\n"]}
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.2",
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.2",
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": "28a566e774a2e108f2583106d51596724c376e37"
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 })
@@ -133,9 +126,31 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
133
126
  setShowJumpToBottomButton(offsetY > 200)
134
127
  }
135
128
 
129
+ // iOS starts the sheet's dismiss gesture when the content scroll view sits at its
130
+ // top edge, which for an inverted list is wherever the newest message rests.
131
+ const restAwayFromDismissEdge = useCallback(() => {
132
+ listRef.current?.scrollToOffset({ animated: false, offset: 1 })
133
+ }, [])
134
+
135
+ const didSetInitialOffset = useRef(false)
136
+ const handleContentSizeChange = useCallback(() => {
137
+ if (didSetInitialOffset.current) return
138
+ didSetInitialOffset.current = true
139
+ restAwayFromDismissEdge()
140
+ }, [restAwayFromDismissEdge])
141
+
142
+ // A negative offset means the bounce is still animating; let it settle first.
143
+ const handleScrollSettled = useCallback(
144
+ (event: any) => {
145
+ const offsetY = event.nativeEvent.contentOffset.y
146
+ if (offsetY >= 0 && offsetY < 1) restAwayFromDismissEdge()
147
+ },
148
+ [restAwayFromDismissEdge]
149
+ )
150
+
136
151
  const handleReturnToBottom = useCallback(() => {
137
152
  listRef.current?.scrollToOffset({
138
- offset: 0,
153
+ offset: 1,
139
154
  })
140
155
  }, [])
141
156
 
@@ -188,6 +203,9 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
188
203
  data={messagesWithSeparators}
189
204
  keyExtractor={item => item.id}
190
205
  onScroll={trackScroll}
206
+ onScrollEndDrag={handleScrollSettled}
207
+ onMomentumScrollEnd={handleScrollSettled}
208
+ onContentSizeChange={handleContentSizeChange}
191
209
  scrollEventThrottle={10}
192
210
  ListFooterComponent={
193
211
  hasNextPage ? null : (
@@ -223,11 +241,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
223
241
  />
224
242
  )
225
243
  }}
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
- }}
244
+ onEndReached={() => fetchNextPage()}
231
245
  ListHeaderComponent={<View style={styles.listHeader} />}
232
246
  />
233
247
  )}
@@ -300,13 +314,11 @@ function InlineDateSeparator({ date }: DateSeparator) {
300
314
  const { isThisYear } = getRelativeDateStatus(date)
301
315
  const showYear = !isThisYear
302
316
  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
317
 
306
318
  return (
307
319
  <View style={styles.container}>
308
320
  <View style={styles.separator} />
309
- <Text variant="footnote" style={styles.dateText} onLayout={onDateTextLayout}>
321
+ <Text variant="footnote" style={styles.dateText}>
310
322
  {dateStamp}
311
323
  </Text>
312
324
  <View style={styles.separator} />
@@ -355,14 +367,11 @@ export const ConversationScreenTitle = ({
355
367
  const resourceType = badge?.pcoResourceType || ''
356
368
  const productName = badge?.appName
357
369
  const name = badge?.text || undefined
358
- // CHAT-476 diagnostics - remove with use_conversation_layout_diagnostics.ts
359
- const onHeaderContainerLayout = useLayoutWidthProbe('header.container', 0)
360
370
 
361
371
  return (
362
372
  <PlatformPressable
363
373
  accessibilityHint="Opens details about members and settings"
364
374
  style={styles.container}
365
- onLayout={onHeaderContainerLayout}
366
375
  onPress={() => {
367
376
  if (deleted) return
368
377
 
@@ -393,12 +402,7 @@ export const ConversationScreenTitle = ({
393
402
  const usePressableHeaderStyle = () => {
394
403
  const { width } = useWindowDimensions()
395
404
  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) }
405
+ const headerContainerStyle = { maxWidth: width - slotReserve * 2 }
402
406
 
403
407
  return StyleSheet.create({
404
408
  container: {