@planningcenter/chat-react-native 3.44.1-qa-876.0 → 3.44.1-qa-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/conversation/attachments/giphy_attachment.js +2 -2
- package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
- package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/image_attachment.js +14 -15
- package/build/components/conversation/attachments/image_attachment.js.map +1 -1
- package/build/components/conversation/attachments/video_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/video_attachment.js +1 -2
- package/build/components/conversation/attachments/video_attachment.js.map +1 -1
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +4 -9
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +6 -10
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js +6 -11
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/components/display/badge.d.ts.map +1 -1
- package/build/components/display/badge.js +3 -16
- package/build/components/display/badge.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts +0 -1
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +2 -10
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +5 -22
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/utils/index.d.ts +0 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +0 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/message_text.d.ts +3 -0
- package/build/utils/message_text.d.ts.map +1 -0
- package/build/utils/message_text.js +8 -0
- package/build/utils/message_text.js.map +1 -0
- package/package.json +3 -3
- package/src/components/conversation/attachments/giphy_attachment.tsx +2 -2
- package/src/components/conversation/attachments/image_attachment.tsx +26 -30
- package/src/components/conversation/attachments/video_attachment.tsx +1 -2
- package/src/components/conversation/message.tsx +4 -9
- package/src/components/conversation/message_form.tsx +8 -17
- package/src/components/conversation/reply_shadow_message.tsx +6 -11
- package/src/components/display/badge.tsx +2 -20
- package/src/hooks/use_conversation_messages.ts +2 -10
- package/src/screens/conversation_screen.tsx +4 -25
- package/src/utils/__tests__/message_text.test.ts +45 -0
- package/src/utils/index.ts +0 -1
- package/src/utils/message_text.ts +9 -0
- package/build/hooks/use_conversation_layout_diagnostics.d.ts +0 -21
- package/build/hooks/use_conversation_layout_diagnostics.d.ts.map +0 -1
- package/build/hooks/use_conversation_layout_diagnostics.js +0 -99
- package/build/hooks/use_conversation_layout_diagnostics.js.map +0 -1
- package/build/utils/aspect_ratio.d.ts +0 -2
- package/build/utils/aspect_ratio.d.ts.map +0 -1
- package/build/utils/aspect_ratio.js +0 -11
- package/build/utils/aspect_ratio.js.map +0 -1
- package/src/__tests__/hooks/use_conversation_layout_diagnostics.test.tsx +0 -142
- package/src/hooks/use_conversation_layout_diagnostics.ts +0 -123
- package/src/utils/__tests__/aspect_ratio.test.ts +0 -50
- package/src/utils/aspect_ratio.ts +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.44.1-qa-
|
|
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-
|
|
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": "
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
181
|
-
const availableWindowHeight =
|
|
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 =
|
|
303
|
-
const windowCenterY =
|
|
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:
|
|
406
|
-
offset:
|
|
405
|
+
length: WINDOW_WIDTH,
|
|
406
|
+
offset: WINDOW_WIDTH * index,
|
|
407
407
|
index,
|
|
408
408
|
}),
|
|
409
|
-
[
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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:
|
|
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:
|
|
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={
|
|
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 {
|
|
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
|
|
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}
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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}
|
|
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
|
+
})
|
package/src/utils/index.ts
CHANGED
|
@@ -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"}
|