@planningcenter/chat-react-native 4.0.1-qa-staging.1 → 4.0.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/conversation/message.d.ts +2 -3
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +22 -23
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_read_receipts.d.ts +4 -2
- package/build/components/conversation/message_read_receipts.d.ts.map +1 -1
- package/build/components/conversation/message_read_receipts.js +3 -2
- package/build/components/conversation/message_read_receipts.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts +1 -2
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js +46 -47
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/components/primitive/form_sheet.d.ts +1 -2
- package/build/components/primitive/form_sheet.d.ts.map +1 -1
- package/build/components/primitive/form_sheet.js +53 -68
- package/build/components/primitive/form_sheet.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +28 -40
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +1 -6
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_message_create_or_update.d.ts +4 -99
- package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
- package/build/hooks/use_message_create_or_update.js +6 -47
- package/build/hooks/use_message_create_or_update.js.map +1 -1
- package/build/screens/conversation_new/components/groups_form.js +80 -71
- package/build/screens/conversation_new/components/groups_form.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +365 -184
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversation_select_type_screen.d.ts.map +1 -1
- package/build/screens/conversation_select_type_screen.js +98 -82
- package/build/screens/conversation_select_type_screen.js.map +1 -1
- package/build/types/resources/message.d.ts +0 -2
- package/build/types/resources/message.d.ts.map +1 -1
- package/build/types/resources/message.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +0 -1
- package/build/utils/cache/messages_cache.d.ts.map +1 -1
- package/build/utils/cache/messages_cache.js +7 -14
- package/build/utils/cache/messages_cache.js.map +1 -1
- package/build/utils/cache/optimistically_create_message.d.ts +1 -2
- package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
- package/build/utils/cache/optimistically_create_message.js +2 -4
- package/build/utils/cache/optimistically_create_message.js.map +1 -1
- package/build/utils/client/transform_response.d.ts.map +1 -1
- package/build/utils/client/transform_response.js +5 -9
- package/build/utils/client/transform_response.js.map +1 -1
- package/build/utils/group_messages.d.ts +0 -1
- package/build/utils/group_messages.d.ts.map +1 -1
- package/build/utils/group_messages.js +1 -10
- package/build/utils/group_messages.js.map +1 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.js +0 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
- package/build/utils/request/messages_data_options.d.ts.map +1 -1
- package/build/utils/request/messages_data_options.js +2 -2
- package/build/utils/request/messages_data_options.js.map +1 -1
- package/package.json +3 -3
- package/src/components/conversation/message.tsx +18 -15
- package/src/components/conversation/message_read_receipts.tsx +5 -3
- package/src/components/conversation/reply_shadow_message.tsx +7 -9
- package/src/components/primitive/form_sheet.tsx +3 -10
- package/src/hooks/use_conversation_messages.ts +1 -5
- package/src/hooks/use_conversation_messages_jolt_events.ts +0 -7
- package/src/hooks/use_message_create_or_update.ts +16 -29
- package/src/screens/conversation_new/components/groups_form.tsx +7 -2
- package/src/screens/conversation_screen.tsx +104 -11
- package/src/screens/conversation_select_type_screen.tsx +9 -3
- package/src/types/resources/message.ts +0 -2
- package/src/utils/__tests__/group_messages.test.ts +0 -48
- package/src/utils/cache/messages_cache.ts +11 -27
- package/src/utils/cache/optimistically_create_message.ts +0 -3
- package/src/utils/client/transform_response.ts +5 -7
- package/src/utils/group_messages.ts +1 -18
- package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +0 -1
- package/src/utils/request/messages_data_options.ts +1 -2
- package/build/components/conversation/message_list.d.ts +0 -21
- package/build/components/conversation/message_list.d.ts.map +0 -1
- package/build/components/conversation/message_list.js +0 -273
- package/build/components/conversation/message_list.js.map +0 -1
- package/build/utils/replace_equal_by_id.d.ts +0 -6
- package/build/utils/replace_equal_by_id.d.ts.map +0 -1
- package/build/utils/replace_equal_by_id.js +0 -41
- package/build/utils/replace_equal_by_id.js.map +0 -1
- package/src/components/conversation/__tests__/message_list.test.tsx +0 -271
- package/src/components/conversation/__tests__/reply_shadow_message.test.tsx +0 -119
- package/src/components/conversation/message_list.tsx +0 -157
- package/src/hooks/__tests__/use_message_create_or_update.test.tsx +0 -110
- package/src/utils/__tests__/replace_equal_by_id.test.ts +0 -73
- package/src/utils/cache/__tests__/messages_cache.test.ts +0 -89
- package/src/utils/cache/__tests__/messages_cache_reply_roots.test.ts +0 -70
- package/src/utils/client/__tests__/transform_response.test.ts +0 -62
- package/src/utils/replace_equal_by_id.ts +0 -42
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useNavigation } from '@react-navigation/native'
|
|
2
2
|
import { some } from 'lodash'
|
|
3
|
-
import React
|
|
3
|
+
import React from 'react'
|
|
4
4
|
import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
5
5
|
import Animated from 'react-native-reanimated'
|
|
6
6
|
import {
|
|
@@ -37,20 +37,18 @@ interface ReplyShadowMessageProps extends MessageResource {
|
|
|
37
37
|
nextRendersAuthor: boolean
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export
|
|
40
|
+
export function ReplyShadowMessage({
|
|
41
41
|
conversation_id,
|
|
42
42
|
inReplyScreen,
|
|
43
43
|
messageId,
|
|
44
|
-
replyRoot,
|
|
45
44
|
isReplyShadowMessage,
|
|
46
45
|
nextRendersAuthor,
|
|
47
46
|
}: ReplyShadowMessageProps) {
|
|
48
|
-
const
|
|
49
|
-
const message = replyRoot ?? fetched.message
|
|
47
|
+
const { message, isError, isLoading } = useConversationMessage({ conversation_id, messageId })
|
|
50
48
|
|
|
51
49
|
if (inReplyScreen) return null
|
|
52
|
-
if (
|
|
53
|
-
if (!message) return <ShadowMessageFallback text="Message deleted" />
|
|
50
|
+
if (isLoading) return <ShadowMessageFallback text="Loading..." />
|
|
51
|
+
if (isError || !message) return <ShadowMessageFallback text="Message deleted" />
|
|
54
52
|
|
|
55
53
|
const enrichedMessage = {
|
|
56
54
|
...message,
|
|
@@ -59,7 +57,7 @@ export const ReplyShadowMessage = memo(function ReplyShadowMessageRow({
|
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
return <ShadowMessageContent conversation_id={conversation_id} {...enrichedMessage} />
|
|
62
|
-
}
|
|
60
|
+
}
|
|
63
61
|
|
|
64
62
|
interface ShadowMessageContentProps extends MessageResource {
|
|
65
63
|
conversation_id: number
|
|
@@ -84,7 +82,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
|
|
|
84
82
|
const handleNavigateToReplies = () => {
|
|
85
83
|
navigation.navigate('ConversationReply', {
|
|
86
84
|
conversation_id,
|
|
87
|
-
reply_root_id: message.replyRootId
|
|
85
|
+
reply_root_id: message.replyRootId,
|
|
88
86
|
reply_root_author_name: message.author.name,
|
|
89
87
|
})
|
|
90
88
|
}
|
|
@@ -85,7 +85,6 @@ interface FormSheetRootProps {
|
|
|
85
85
|
style?: StyleProp<ViewStyle>
|
|
86
86
|
contentStyle?: StyleProp<ViewStyle>
|
|
87
87
|
showAndroidSheetGrabber?: boolean
|
|
88
|
-
fitToContents?: boolean
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
export function FormSheetRoot({
|
|
@@ -93,9 +92,8 @@ export function FormSheetRoot({
|
|
|
93
92
|
style,
|
|
94
93
|
contentStyle,
|
|
95
94
|
showAndroidSheetGrabber = true,
|
|
96
|
-
fitToContents = false,
|
|
97
95
|
}: FormSheetRootProps) {
|
|
98
|
-
const styles = useStyles(
|
|
96
|
+
const styles = useStyles()
|
|
99
97
|
|
|
100
98
|
return (
|
|
101
99
|
<View style={[styles.container, style]} collapsable={false}>
|
|
@@ -300,21 +298,16 @@ FormSheetAction.displayName = 'FormSheet.Action'
|
|
|
300
298
|
interface Styles {
|
|
301
299
|
appearance?: FormSheetActionAppearanceUnion
|
|
302
300
|
disabled?: boolean
|
|
303
|
-
fitToContents?: boolean
|
|
304
301
|
}
|
|
305
302
|
|
|
306
|
-
const useStyles = ({
|
|
307
|
-
appearance = 'neutral',
|
|
308
|
-
disabled = false,
|
|
309
|
-
fitToContents = false,
|
|
310
|
-
}: Styles = {}) => {
|
|
303
|
+
const useStyles = ({ appearance = 'neutral', disabled = false }: Styles = {}) => {
|
|
311
304
|
const { colors } = useTheme()
|
|
312
305
|
const { height } = useWindowDimensions()
|
|
313
306
|
const { bottom, top } = useSafeAreaInsets()
|
|
314
307
|
const headerHeight = useHeaderHeight()
|
|
315
308
|
|
|
316
309
|
const containerHeight = Platform.select({
|
|
317
|
-
ios:
|
|
310
|
+
ios: height - top - headerHeight,
|
|
318
311
|
default: null,
|
|
319
312
|
})
|
|
320
313
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import { MessageResource } from '../types'
|
|
3
|
-
import { replaceEqualById } from '../utils/replace_equal_by_id'
|
|
4
3
|
import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
|
|
5
4
|
import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
|
|
6
5
|
|
|
@@ -11,10 +10,7 @@ export const useConversationMessages = (
|
|
|
11
10
|
const { data, refetch, isRefetching, fetchNextPage, hasNextPage } =
|
|
12
11
|
useSuspensePaginator<MessageResource>(
|
|
13
12
|
getMessagesRequestArgs({ conversation_id, reply_root_id }),
|
|
14
|
-
|
|
15
|
-
structuralSharing: replaceEqualById,
|
|
16
|
-
...opts,
|
|
17
|
-
}
|
|
13
|
+
opts
|
|
18
14
|
)
|
|
19
15
|
const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
|
|
20
16
|
const messages = useMemo(
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
updateCacheWithMessage,
|
|
12
12
|
updateCacheWithIndividualMessage,
|
|
13
13
|
updateCacheWithReaction,
|
|
14
|
-
updateCacheReplyRoots,
|
|
15
14
|
getThreadedMessagesQueryKey,
|
|
16
15
|
} from '../utils/cache/messages_cache'
|
|
17
16
|
import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
|
|
@@ -45,7 +44,6 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
45
44
|
data,
|
|
46
45
|
currentPersonId: currentPerson.id,
|
|
47
46
|
})
|
|
48
|
-
const isReplyRootUpdate = e.event === 'message.updated' && data.reply_root_id === data.sort_key
|
|
49
47
|
|
|
50
48
|
if (e.event === 'message.created' && data.author_id) {
|
|
51
49
|
removeAllTypingEventsByAuthorId(data.author_id)
|
|
@@ -64,11 +62,6 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
64
62
|
data.reply_root_id
|
|
65
63
|
)
|
|
66
64
|
updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
|
|
67
|
-
|
|
68
|
-
if (isReplyRootUpdate) {
|
|
69
|
-
updateCacheReplyRoots(queryClient, messagesQueryKey, message)
|
|
70
|
-
updateCacheReplyRoots(queryClient, threadedMessagesQueryKey, message)
|
|
71
|
-
}
|
|
72
65
|
}
|
|
73
66
|
|
|
74
67
|
// Update individual message query cache so reply shadow messages update immediately
|
|
@@ -22,14 +22,6 @@ interface Props {
|
|
|
22
22
|
replyRootId?: string | null
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
interface MessageMutationVariables {
|
|
26
|
-
text: string
|
|
27
|
-
attachments?: DenormalizedAttachmentResourceForCreate[]
|
|
28
|
-
idempotentKey?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type ResolvedMessageMutationVariables = MessageMutationVariables & { idempotentKey: string }
|
|
32
|
-
|
|
33
25
|
export function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {
|
|
34
26
|
const messageId = message?.id || null
|
|
35
27
|
const isEditing = !isNewMessage(message)
|
|
@@ -37,7 +29,13 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
37
29
|
const currentPerson = useCurrentPerson()
|
|
38
30
|
|
|
39
31
|
const mutation = useMutation({
|
|
40
|
-
mutationFn: ({
|
|
32
|
+
mutationFn: ({
|
|
33
|
+
text,
|
|
34
|
+
attachments,
|
|
35
|
+
}: {
|
|
36
|
+
text: string
|
|
37
|
+
attachments?: DenormalizedAttachmentResourceForCreate[]
|
|
38
|
+
}) => {
|
|
41
39
|
const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })
|
|
42
40
|
const fieldsWithValueJoined = Object.fromEntries(
|
|
43
41
|
Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
|
|
@@ -48,6 +46,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
48
46
|
...(!isEditing && replyRootId ? { reply_root: { id: replyRootId } } : {}),
|
|
49
47
|
}
|
|
50
48
|
if (!isEditing) {
|
|
49
|
+
const idempotentKey = insecureUUID()
|
|
51
50
|
attributes.idempotent_key = idempotentKey
|
|
52
51
|
startMessageCreationTracking(idempotentKey)
|
|
53
52
|
}
|
|
@@ -72,7 +71,13 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
72
71
|
})
|
|
73
72
|
}
|
|
74
73
|
},
|
|
75
|
-
onMutate: async ({
|
|
74
|
+
onMutate: async ({
|
|
75
|
+
text,
|
|
76
|
+
attachments,
|
|
77
|
+
}: {
|
|
78
|
+
text: string
|
|
79
|
+
attachments?: DenormalizedAttachmentResourceForCreate[]
|
|
80
|
+
}) => {
|
|
76
81
|
if (message && isEditing) {
|
|
77
82
|
const optimisticMessage = optimisticallyUpdateMessage({
|
|
78
83
|
conversationId,
|
|
@@ -91,7 +96,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
91
96
|
attachments,
|
|
92
97
|
currentPerson,
|
|
93
98
|
replyRootId,
|
|
94
|
-
idempotentKey,
|
|
95
99
|
})
|
|
96
100
|
|
|
97
101
|
return { message: optimisticMessage }
|
|
@@ -151,24 +155,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
|
|
|
151
155
|
},
|
|
152
156
|
})
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
variables: MessageMutationVariables
|
|
156
|
-
): ResolvedMessageMutationVariables => ({
|
|
157
|
-
...variables,
|
|
158
|
-
idempotentKey: variables.idempotentKey ?? message?.idempotentKey ?? insecureUUID(),
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
const mutate = (
|
|
162
|
-
variables: MessageMutationVariables,
|
|
163
|
-
options?: Parameters<typeof mutation.mutate>[1]
|
|
164
|
-
) => mutation.mutate(resolveVariables(variables), options)
|
|
165
|
-
|
|
166
|
-
const mutateAsync = (
|
|
167
|
-
variables: MessageMutationVariables,
|
|
168
|
-
options?: Parameters<typeof mutation.mutateAsync>[1]
|
|
169
|
-
) => mutation.mutateAsync(resolveVariables(variables), options)
|
|
170
|
-
|
|
171
|
-
return { ...mutation, mutate, mutateAsync }
|
|
158
|
+
return mutation
|
|
172
159
|
}
|
|
173
160
|
|
|
174
161
|
/**
|
|
@@ -212,9 +212,11 @@ function FormContent({
|
|
|
212
212
|
</View>
|
|
213
213
|
<Divider />
|
|
214
214
|
<Pressable style={styles.titleSection} onPress={handleTitleSectionPress}>
|
|
215
|
-
<Heading variant="h3">
|
|
215
|
+
<Heading variant="h3">
|
|
216
|
+
Title <Text style={styles.required}>*</Text>
|
|
217
|
+
</Heading>
|
|
216
218
|
<AutoFocusAwareTextInput
|
|
217
|
-
placeholder="Topic of conversation
|
|
219
|
+
placeholder="Topic of conversation"
|
|
218
220
|
value={title}
|
|
219
221
|
onChangeText={setTitle}
|
|
220
222
|
style={styles.titleInput}
|
|
@@ -291,6 +293,9 @@ const useStyles = () => {
|
|
|
291
293
|
fontSize: 18,
|
|
292
294
|
color: colors.textColorDefaultPrimary,
|
|
293
295
|
},
|
|
296
|
+
required: {
|
|
297
|
+
color: colors.statusErrorText,
|
|
298
|
+
},
|
|
294
299
|
memberSection: {
|
|
295
300
|
padding: sectionPadding,
|
|
296
301
|
paddingBottom: 0,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { date as formatDate } from '@planningcenter/datetime-fmt'
|
|
1
2
|
import { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'
|
|
2
3
|
import {
|
|
3
4
|
CommonActions,
|
|
@@ -10,21 +11,24 @@ import {
|
|
|
10
11
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
11
12
|
import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
12
13
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
13
|
-
import { Badge, Icon } from '../components'
|
|
14
|
+
import { Badge, Icon, Text } from '../components'
|
|
14
15
|
import { ConversationStartBanner } from '../components/conversation/conversation_start_banner'
|
|
15
16
|
import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
|
|
17
|
+
import { Message } from '../components/conversation/message'
|
|
16
18
|
import { MessageForm } from '../components/conversation/message_form'
|
|
17
|
-
import { MessageList } from '../components/conversation/message_list'
|
|
18
19
|
import {
|
|
19
20
|
ConversationDisabledBanner,
|
|
20
21
|
LeaderMessagesDisabledBanner,
|
|
21
22
|
MemberMessagesDisabledBanner,
|
|
22
23
|
} from '../components/conversation/messages_disabled_banners'
|
|
24
|
+
import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
|
|
25
|
+
import { SystemMessage } from '../components/conversation/system_message'
|
|
23
26
|
import { TypingIndicator } from '../components/conversation/typing_indicator'
|
|
24
27
|
import { KeyboardView } from '../components/display/keyboard_view'
|
|
25
28
|
import { useHeaderSlotReserve } from '../components/display/platform_modal_header_buttons'
|
|
26
29
|
import BlankState from '../components/primitive/blank_state_primitive'
|
|
27
30
|
import { ConversationContextProvider } from '../contexts/conversation_context'
|
|
31
|
+
import { useTheme } from '../hooks'
|
|
28
32
|
import { useMessageAvailableMembersAction } from '../hooks/direct_messages/use_message_available_members_action'
|
|
29
33
|
import { useConversation } from '../hooks/use_conversation'
|
|
30
34
|
import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
|
|
@@ -41,6 +45,10 @@ import { useVideoModerationJoltEvents } from '../hooks/use_video_moderation_jolt
|
|
|
41
45
|
import { ConversationResource } from '../types/resources/conversation'
|
|
42
46
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge'
|
|
43
47
|
import { MessageResource } from '../types/resources/message'
|
|
48
|
+
import { getRelativeDateStatus } from '../utils/date'
|
|
49
|
+
import { groupMessages, type DateSeparator } from '../utils/group_messages'
|
|
50
|
+
import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
|
|
51
|
+
import { isSystemMessage } from '../utils/system_messages'
|
|
44
52
|
|
|
45
53
|
export type ConversationRouteProps = {
|
|
46
54
|
conversation_id: number
|
|
@@ -93,11 +101,16 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
93
101
|
const { markHandledByMessageForm } = useVideoModerationJoltEvents()
|
|
94
102
|
useEnsureConversationsRouteExists()
|
|
95
103
|
useMarkLatestMessageRead({ conversation, messages })
|
|
96
|
-
const
|
|
104
|
+
const messagesWithSeparators = groupMessages({
|
|
105
|
+
ms: messages,
|
|
106
|
+
inReplyScreen: !!reply_root_id,
|
|
107
|
+
})
|
|
108
|
+
const noMessages = messagesWithSeparators.length === 0
|
|
97
109
|
|
|
98
110
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
99
111
|
const canReply = memberAbility?.canReply
|
|
100
112
|
const showLeaderDisabledReplyBanner = canReply && repliesDisabled
|
|
113
|
+
const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
|
|
101
114
|
const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))
|
|
102
115
|
const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]
|
|
103
116
|
const replyHeaderTitle = replyRootAuthorFirstName
|
|
@@ -168,19 +181,54 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
168
181
|
<ConversationStartBanner conversation={conversation} hasMessages={false} />
|
|
169
182
|
</View>
|
|
170
183
|
) : (
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
hasNextPage={hasNextPage}
|
|
178
|
-
onEndReached={() => fetchNextPage()}
|
|
184
|
+
<FlatList
|
|
185
|
+
inverted
|
|
186
|
+
ref={listRef}
|
|
187
|
+
contentContainerStyle={styles.listContainer}
|
|
188
|
+
data={messagesWithSeparators}
|
|
189
|
+
keyExtractor={item => item.id}
|
|
179
190
|
onScroll={trackScroll}
|
|
180
191
|
onLayout={onLayout}
|
|
181
192
|
onContentSizeChange={onContentSizeChange}
|
|
182
193
|
onScrollEndDrag={onScrollEndDrag}
|
|
183
194
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
195
|
+
scrollEventThrottle={10}
|
|
196
|
+
ListFooterComponent={
|
|
197
|
+
hasNextPage ? null : (
|
|
198
|
+
<ConversationStartBanner conversation={conversation} hasMessages={true} />
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
renderItem={({ item }) => {
|
|
202
|
+
if (item.type === 'DateSeparator') {
|
|
203
|
+
return <InlineDateSeparator {...item} />
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (item.type === 'ReplyShadowMessage') {
|
|
207
|
+
return (
|
|
208
|
+
<ReplyShadowMessage
|
|
209
|
+
{...(item as any)}
|
|
210
|
+
conversation_id={conversation_id}
|
|
211
|
+
inReplyScreen={!!reply_root_id}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isSystemMessage(item)) {
|
|
217
|
+
return <SystemMessage message={item} conversationId={conversation_id} />
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<Message
|
|
222
|
+
{...item}
|
|
223
|
+
canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
|
|
224
|
+
conversation_id={conversation_id}
|
|
225
|
+
latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
|
|
226
|
+
inReplyScreen={!!reply_root_id}
|
|
227
|
+
/>
|
|
228
|
+
)
|
|
229
|
+
}}
|
|
230
|
+
onEndReached={() => fetchNextPage()}
|
|
231
|
+
ListHeaderComponent={<View style={styles.listHeader} />}
|
|
184
232
|
/>
|
|
185
233
|
)}
|
|
186
234
|
<JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
|
|
@@ -247,6 +295,44 @@ function ConversationBottomBar({
|
|
|
247
295
|
)
|
|
248
296
|
}
|
|
249
297
|
|
|
298
|
+
function InlineDateSeparator({ date }: DateSeparator) {
|
|
299
|
+
const styles = useDateSeparatorStyles()
|
|
300
|
+
const { isThisYear } = getRelativeDateStatus(date)
|
|
301
|
+
const showYear = !isThisYear
|
|
302
|
+
const dateStamp = formatDate(date, { style: 'long', year: showYear })
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<View style={styles.container}>
|
|
306
|
+
<View style={styles.separator} />
|
|
307
|
+
<Text variant="footnote" style={styles.dateText}>
|
|
308
|
+
{dateStamp}
|
|
309
|
+
</Text>
|
|
310
|
+
<View style={styles.separator} />
|
|
311
|
+
</View>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const useDateSeparatorStyles = () => {
|
|
316
|
+
const theme = useTheme()
|
|
317
|
+
return StyleSheet.create({
|
|
318
|
+
container: {
|
|
319
|
+
alignItems: 'center',
|
|
320
|
+
flexDirection: 'row',
|
|
321
|
+
paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
|
|
322
|
+
paddingVertical: 16,
|
|
323
|
+
},
|
|
324
|
+
separator: {
|
|
325
|
+
flex: 1,
|
|
326
|
+
height: 1,
|
|
327
|
+
borderTopWidth: 1,
|
|
328
|
+
borderTopColor: theme.colors.borderColorDefaultBase,
|
|
329
|
+
},
|
|
330
|
+
dateText: {
|
|
331
|
+
paddingHorizontal: 8,
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
250
336
|
interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
251
337
|
conversation_id: number
|
|
252
338
|
badge?: ConversationBadgeResource
|
|
@@ -343,6 +429,13 @@ const useStyles = () => {
|
|
|
343
429
|
flex: 1,
|
|
344
430
|
justifyContent: 'center',
|
|
345
431
|
},
|
|
432
|
+
listContainer: {
|
|
433
|
+
paddingVertical: 12,
|
|
434
|
+
},
|
|
435
|
+
listHeader: {
|
|
436
|
+
// Just whitespace to provide space where the typing indicator can be
|
|
437
|
+
height: 16,
|
|
438
|
+
},
|
|
346
439
|
})
|
|
347
440
|
}
|
|
348
441
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
|
2
|
-
import React from 'react'
|
|
2
|
+
import React, { useLayoutEffect } from 'react'
|
|
3
3
|
import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
|
|
4
4
|
import { useCurrentPerson } from '../hooks/use_current_person'
|
|
5
5
|
import { useConversationTypeAccess } from '../hooks/use_new_conversation_entry'
|
|
@@ -10,7 +10,6 @@ import { Haptic } from '../utils/native_adapters'
|
|
|
10
10
|
|
|
11
11
|
export const ConversationSelectTypeScreenOptions = getFormSheetScreenOptions({
|
|
12
12
|
headerTitle: 'New conversation',
|
|
13
|
-
sheetAllowedDetents: 'fitToContents',
|
|
14
13
|
})
|
|
15
14
|
|
|
16
15
|
export type ConversationSelectTypeScreenProps = StaticScreenProps<{
|
|
@@ -35,6 +34,13 @@ export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeSc
|
|
|
35
34
|
const filterParams = { chat_group_graph_id, group_source_app_name }
|
|
36
35
|
const showDirectMessageOption = directMessagesEligible
|
|
37
36
|
|
|
37
|
+
// sheetAllowedDetents depends on DM eligibility, which isn't known until render, so it's set
|
|
38
|
+
// here instead of in the static ConversationSelectTypeScreenOptions. useLayoutEffect applies
|
|
39
|
+
// it before paint so the sheet never visibly resizes after opening.
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
navigation.setOptions({ sheetAllowedDetents: showDirectMessageOption ? [0.3] : [0.25] })
|
|
42
|
+
}, [navigation, showDirectMessageOption])
|
|
43
|
+
|
|
38
44
|
const handleFilterPress = () => {
|
|
39
45
|
if (!newConversationFromFilter) return
|
|
40
46
|
|
|
@@ -76,7 +82,7 @@ export function ConversationSelectTypeScreen({ route }: ConversationSelectTypeSc
|
|
|
76
82
|
navigateToNewConversationType('ConversationSelectDirectMessageRecipients')
|
|
77
83
|
|
|
78
84
|
return (
|
|
79
|
-
<FormSheet.Root
|
|
85
|
+
<FormSheet.Root>
|
|
80
86
|
<FormSheet.Header>
|
|
81
87
|
<FormSheet.HeaderTitle>Start a conversation by</FormSheet.HeaderTitle>
|
|
82
88
|
</FormSheet.Header>
|
|
@@ -17,8 +17,6 @@ export interface MessageResource {
|
|
|
17
17
|
reactionCounts: ReactionCountResource[]
|
|
18
18
|
replyCount: number
|
|
19
19
|
replyRootId?: string | null
|
|
20
|
-
replyRoot?: MessageResource | null
|
|
21
|
-
idempotentKey?: string | null
|
|
22
20
|
messageType: string
|
|
23
21
|
personIdsForSystemEvent?: number[] | null
|
|
24
22
|
systemTextParts?: { names: string[]; overflowCount: number; action: string } | null
|
|
@@ -124,54 +124,6 @@ describe('groupMessages — nextRendersAuthor mirrors the newer enriched neighbo
|
|
|
124
124
|
})
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
describe('groupMessages — reply roots', () => {
|
|
128
|
-
const findShadow = (items: ReturnType<typeof groupMessages>, id: string) =>
|
|
129
|
-
items.find(item => 'type' in item && item.type === 'ReplyShadowMessage' && item.id === id)
|
|
130
|
-
|
|
131
|
-
it('carries the root from the page onto the reply and its shadow', () => {
|
|
132
|
-
const messages = [
|
|
133
|
-
message('03', { replyRootId: 'rootA' }),
|
|
134
|
-
message('02'),
|
|
135
|
-
message('rootA', { replyRootId: 'rootA', text: 'fresh' }),
|
|
136
|
-
]
|
|
137
|
-
|
|
138
|
-
const enriched = groupMessages({ ms: messages })
|
|
139
|
-
|
|
140
|
-
expect((findEnriched(enriched, '03') as MessageResource).replyRoot?.text).toBe('fresh')
|
|
141
|
-
expect(findShadow(enriched, '03-rootA')).toMatchObject({ replyRoot: { id: 'rootA' } })
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('prefers the page copy of the root over the reply’s included snapshot', () => {
|
|
145
|
-
const snapshot = message('rootA', { replyRootId: 'rootA', text: 'stale' })
|
|
146
|
-
const messages = [
|
|
147
|
-
message('03', { replyRootId: 'rootA', replyRoot: snapshot }),
|
|
148
|
-
message('02'),
|
|
149
|
-
message('rootA', { replyRootId: 'rootA', text: 'fresh' }),
|
|
150
|
-
]
|
|
151
|
-
|
|
152
|
-
const enriched = groupMessages({ ms: messages })
|
|
153
|
-
|
|
154
|
-
expect(findShadow(enriched, '03-rootA')).toMatchObject({ replyRoot: { text: 'fresh' } })
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('falls back to the included snapshot when the root is not on the page', () => {
|
|
158
|
-
const snapshot = message('rootA', { replyRootId: 'rootA', text: 'included' })
|
|
159
|
-
const messages = [message('02', { replyRootId: 'rootA', replyRoot: snapshot })]
|
|
160
|
-
|
|
161
|
-
const enriched = groupMessages({ ms: messages })
|
|
162
|
-
|
|
163
|
-
expect(findShadow(enriched, '02-rootA')).toMatchObject({ replyRoot: { text: 'included' } })
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('leaves replyRoot null when the root is neither on the page nor included', () => {
|
|
167
|
-
const messages = [message('02', { replyRootId: 'rootA' })]
|
|
168
|
-
|
|
169
|
-
const enriched = groupMessages({ ms: messages })
|
|
170
|
-
|
|
171
|
-
expect(findShadow(enriched, '02-rootA')).toMatchObject({ replyRoot: null })
|
|
172
|
-
})
|
|
173
|
-
})
|
|
174
|
-
|
|
175
127
|
describe('groupMessages — system messages', () => {
|
|
176
128
|
it('flags lastInGroup true and renderAuthor false on system messages', () => {
|
|
177
129
|
const messages = [
|
|
@@ -4,11 +4,7 @@ import { ApiCollection, ApiResource, MessageResource } from '../../types'
|
|
|
4
4
|
import { JoltReactionEvent } from '../../types/jolt_events'
|
|
5
5
|
import { transformReactionEventDataToReactionCountResource } from '../jolt/transform_reaction_event_data_to_reaction_count_resource'
|
|
6
6
|
import { getMessagesRequestArgs } from '../request/get_messages'
|
|
7
|
-
import {
|
|
8
|
-
updateAllRecordsInPagesData,
|
|
9
|
-
updateOrCreateRecordInPagesData,
|
|
10
|
-
updateRecordInPagesData,
|
|
11
|
-
} from './page_mutations'
|
|
7
|
+
import { updateOrCreateRecordInPagesData, updateRecordInPagesData } from './page_mutations'
|
|
12
8
|
import { deleteRecordInPagesData } from './page_mutations'
|
|
13
9
|
|
|
14
10
|
export function updateCacheWithMessage(
|
|
@@ -19,16 +15,20 @@ export function updateCacheWithMessage(
|
|
|
19
15
|
) {
|
|
20
16
|
queryClient.setQueryData<MessagesQueryData>(queryKey, prev => {
|
|
21
17
|
if (event === 'message.created') {
|
|
22
|
-
// Before adding the new message, remove
|
|
23
|
-
//
|
|
18
|
+
// Before adding the new message, remove any pending temporary messages
|
|
19
|
+
// with matching text to prevent duplicates from race conditions
|
|
24
20
|
let dataAfterTempRemoval = prev
|
|
25
|
-
if (prev && message.
|
|
21
|
+
if (prev && message.text && message.mine) {
|
|
26
22
|
dataAfterTempRemoval = deleteRecordInPagesData({
|
|
27
23
|
data: prev,
|
|
28
24
|
record: message,
|
|
29
|
-
matchFn: existingMessage =>
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
matchFn: (existingMessage, _record) => {
|
|
26
|
+
return (
|
|
27
|
+
isTemporaryMessageId(existingMessage.id) &&
|
|
28
|
+
existingMessage.text === message.text &&
|
|
29
|
+
existingMessage.mine
|
|
30
|
+
)
|
|
31
|
+
},
|
|
32
32
|
})
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -70,22 +70,6 @@ export function updateCacheWithIndividualMessage(
|
|
|
70
70
|
})
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function updateCacheReplyRoots(
|
|
74
|
-
queryClient: QueryClient,
|
|
75
|
-
queryKey: unknown[],
|
|
76
|
-
root: MessageResource
|
|
77
|
-
) {
|
|
78
|
-
queryClient.setQueryData<MessagesQueryData>(queryKey, prev =>
|
|
79
|
-
updateAllRecordsInPagesData({
|
|
80
|
-
data: prev,
|
|
81
|
-
processRecord: message =>
|
|
82
|
-
message.replyRoot?.id === root.id
|
|
83
|
-
? { ...message, replyRoot: mergeMessageUpdate(root, message.replyRoot) }
|
|
84
|
-
: message,
|
|
85
|
-
})
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
73
|
export function updateCacheWithReaction(
|
|
90
74
|
queryClient: QueryClient,
|
|
91
75
|
queryKey: unknown[],
|
|
@@ -15,7 +15,6 @@ export function optimisticallyCreateMessage({
|
|
|
15
15
|
currentPerson,
|
|
16
16
|
message,
|
|
17
17
|
replyRootId,
|
|
18
|
-
idempotentKey,
|
|
19
18
|
}: {
|
|
20
19
|
conversationId: number
|
|
21
20
|
text: string
|
|
@@ -23,7 +22,6 @@ export function optimisticallyCreateMessage({
|
|
|
23
22
|
currentPerson: CurrentPersonResource
|
|
24
23
|
message?: MessageResource
|
|
25
24
|
replyRootId?: string | null
|
|
26
|
-
idempotentKey?: string
|
|
27
25
|
}) {
|
|
28
26
|
const id = message?.id || generateTempMessageId()
|
|
29
27
|
|
|
@@ -55,7 +53,6 @@ export function optimisticallyCreateMessage({
|
|
|
55
53
|
pending: true,
|
|
56
54
|
replyCount: 0,
|
|
57
55
|
replyRootId: replyRootId || null,
|
|
58
|
-
idempotentKey,
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
// Add the optimistic message to the cache
|
|
@@ -32,8 +32,7 @@ const transformRelationships = relationships => {
|
|
|
32
32
|
}, {})
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const addIncludedRecords = (
|
|
36
|
-
const { relationships } = record
|
|
35
|
+
const addIncludedRecords = (relationships, included, seen, parent) => {
|
|
37
36
|
return Object.keys(relationships || {}).reduce((acc, key) => {
|
|
38
37
|
const relationship = relationships[key]
|
|
39
38
|
if (_.isArray(relationship.data)) {
|
|
@@ -44,14 +43,13 @@ const addIncludedRecords = (record, included, seen, parent) => {
|
|
|
44
43
|
return _.set(
|
|
45
44
|
acc,
|
|
46
45
|
_.pluralize(key),
|
|
47
|
-
_.map(records,
|
|
46
|
+
_.map(records, record => transformRecord(record, included, seen))
|
|
48
47
|
)
|
|
49
48
|
} else {
|
|
50
49
|
const [id, type] = _.at(relationship, ['data.id', 'data.type'])
|
|
51
50
|
if (id && type) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
return _.set(acc, key, related ? transformRecord(related, included, seen) : null)
|
|
51
|
+
const record = _.find(included, { id, type })
|
|
52
|
+
return _.set(acc, key, record ? transformRecord(record, included, seen) : null)
|
|
55
53
|
} else {
|
|
56
54
|
return acc
|
|
57
55
|
}
|
|
@@ -80,7 +78,7 @@ const transformRecord = (record, included, seen) => {
|
|
|
80
78
|
|
|
81
79
|
seen.set(record, transformed)
|
|
82
80
|
|
|
83
|
-
return addIncludedRecords(
|
|
81
|
+
return addIncludedRecords(relationships, included, seen, transformed)
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
84
|
|