@planningcenter/chat-react-native 3.17.0-rc.0 → 3.17.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.
Files changed (93) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +27 -7
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_form.d.ts +3 -2
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +6 -4
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversation/reply_connectors.d.ts +1 -0
  9. package/build/components/conversation/reply_connectors.d.ts.map +1 -1
  10. package/build/components/conversation/reply_connectors.js +33 -14
  11. package/build/components/conversation/reply_connectors.js.map +1 -1
  12. package/build/components/conversation/reply_shadow_message.d.ts +12 -0
  13. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -0
  14. package/build/components/conversation/{shadow_message.js → reply_shadow_message.js} +36 -6
  15. package/build/components/conversation/reply_shadow_message.js.map +1 -0
  16. package/build/hooks/use_conversation_message.d.ts +12 -0
  17. package/build/hooks/use_conversation_message.d.ts.map +1 -0
  18. package/build/hooks/use_conversation_message.js +11 -0
  19. package/build/hooks/use_conversation_message.js.map +1 -0
  20. package/build/hooks/use_conversation_messages.d.ts +1 -20
  21. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  22. package/build/hooks/use_conversation_messages.js +2 -33
  23. package/build/hooks/use_conversation_messages.js.map +1 -1
  24. package/build/hooks/use_conversation_messages_jolt_events.js +1 -1
  25. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  26. package/build/hooks/use_message_create_or_update.d.ts +1 -1
  27. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  28. package/build/hooks/use_message_create_or_update.js +1 -1
  29. package/build/hooks/use_message_create_or_update.js.map +1 -1
  30. package/build/hooks/use_message_reaction_toggle.js +1 -1
  31. package/build/hooks/use_message_reaction_toggle.js.map +1 -1
  32. package/build/navigation/index.d.ts +6 -2
  33. package/build/navigation/index.d.ts.map +1 -1
  34. package/build/navigation/index.js +3 -3
  35. package/build/navigation/index.js.map +1 -1
  36. package/build/screens/conversation_screen.d.ts +11 -3
  37. package/build/screens/conversation_screen.d.ts.map +1 -1
  38. package/build/screens/conversation_screen.js +46 -10
  39. package/build/screens/conversation_screen.js.map +1 -1
  40. package/build/screens/message_actions_screen.d.ts +1 -0
  41. package/build/screens/message_actions_screen.d.ts.map +1 -1
  42. package/build/screens/message_actions_screen.js +5 -5
  43. package/build/screens/message_actions_screen.js.map +1 -1
  44. package/build/types/resources/message.d.ts +3 -0
  45. package/build/types/resources/message.d.ts.map +1 -1
  46. package/build/types/resources/message.js.map +1 -1
  47. package/build/utils/cache/optimistically_create_message.js +1 -1
  48. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  49. package/build/utils/cache/optimistically_update_message.js +1 -1
  50. package/build/utils/cache/optimistically_update_message.js.map +1 -1
  51. package/build/utils/pluralize.js +1 -1
  52. package/build/utils/pluralize.js.map +1 -1
  53. package/build/utils/request/get_message.d.ts +20 -0
  54. package/build/utils/request/get_message.d.ts.map +1 -0
  55. package/build/utils/request/get_message.js +18 -0
  56. package/build/utils/request/get_message.js.map +1 -0
  57. package/build/utils/request/get_messages.d.ts +20 -0
  58. package/build/utils/request/get_messages.d.ts.map +1 -0
  59. package/build/utils/request/get_messages.js +20 -0
  60. package/build/utils/request/get_messages.js.map +1 -0
  61. package/build/utils/request/messages_data_options.d.ts +7 -0
  62. package/build/utils/request/messages_data_options.d.ts.map +1 -0
  63. package/build/utils/request/messages_data_options.js +18 -0
  64. package/build/utils/request/messages_data_options.js.map +1 -0
  65. package/package.json +2 -2
  66. package/src/__tests__/utils/pluralize.tsx +3 -0
  67. package/src/components/conversation/message.tsx +31 -7
  68. package/src/components/conversation/message_form.tsx +10 -4
  69. package/src/components/conversation/reply_connectors.tsx +42 -20
  70. package/src/components/conversation/{shadow_message.tsx → reply_shadow_message.tsx} +55 -6
  71. package/src/hooks/use_conversation_message.ts +25 -0
  72. package/src/hooks/use_conversation_messages.ts +3 -53
  73. package/src/hooks/use_conversation_messages_jolt_events.ts +1 -1
  74. package/src/hooks/use_message_create_or_update.ts +2 -2
  75. package/src/hooks/use_message_reaction_toggle.ts +1 -1
  76. package/src/navigation/index.tsx +3 -3
  77. package/src/screens/conversation_screen.tsx +68 -12
  78. package/src/screens/message_actions_screen.tsx +13 -3
  79. package/src/types/resources/message.ts +3 -0
  80. package/src/utils/cache/optimistically_create_message.ts +1 -1
  81. package/src/utils/cache/optimistically_update_message.ts +1 -1
  82. package/src/utils/pluralize.ts +1 -1
  83. package/src/utils/request/get_message.ts +32 -0
  84. package/src/utils/request/get_messages.ts +34 -0
  85. package/src/utils/request/messages_data_options.ts +18 -0
  86. package/build/components/conversation/shadow_message.d.ts +0 -8
  87. package/build/components/conversation/shadow_message.d.ts.map +0 -1
  88. package/build/components/conversation/shadow_message.js.map +0 -1
  89. package/build/utils/request/messages.d.ts +0 -15
  90. package/build/utils/request/messages.d.ts.map +0 -1
  91. package/build/utils/request/messages.js +0 -22
  92. package/build/utils/request/messages.js.map +0 -1
  93. package/src/utils/request/messages.ts +0 -21
@@ -10,6 +10,8 @@ import { REPLIES_FEATURE_ENABLED } from '../../utils'
10
10
 
11
11
  const MY_REPLY_CONNECTOR_WIDTH = 38
12
12
  const CONNECTOR_BORDER_WIDTH = 4
13
+ const OFFSET_CONNECTOR_CONTAINER = CONNECTOR_BORDER_WIDTH / 2 // Accounts for the border width ensuring the connectors are centered under the avatar
14
+ export const AVATAR_CONNECTOR_SPACING = 8
13
15
 
14
16
  interface ReplyConnectorProps {
15
17
  message: MessageResource
@@ -18,8 +20,10 @@ interface ReplyConnectorProps {
18
20
 
19
21
  export function TheirReplyConnector({ message, messageBubbleHeight }: ReplyConnectorProps) {
20
22
  const styles = useStyles()
23
+ const { nextRendersAuthor, threadPosition, renderAuthor, isReplyShadowMessage } = message
21
24
 
22
25
  if (!REPLIES_FEATURE_ENABLED) return null
26
+ if (messageBubbleHeight === 0) return null // Prevents UI shifting
23
27
 
24
28
  const connectorMap: Record<string, React.ReactNode | null> = {
25
29
  shortTailCurve: <ShortTailCurveConnector height={messageBubbleHeight / 2} />,
@@ -29,11 +33,13 @@ export function TheirReplyConnector({ message, messageBubbleHeight }: ReplyConne
29
33
  }
30
34
 
31
35
  const getConnectorKey = () => {
32
- const { threadPosition, renderAuthor } = message
33
-
34
36
  if (threadPosition === 'first' && !renderAuthor) return 'shortHeadCurve'
35
37
  if (threadPosition === 'last' && !renderAuthor) return 'shortTailCurve'
36
- if ((threadPosition === 'first' && renderAuthor) || threadPosition === 'center')
38
+ if (
39
+ (threadPosition === 'first' && renderAuthor) ||
40
+ threadPosition === 'center' ||
41
+ isReplyShadowMessage
42
+ )
37
43
  return 'verticalLine'
38
44
  return 'default'
39
45
  }
@@ -41,13 +47,20 @@ export function TheirReplyConnector({ message, messageBubbleHeight }: ReplyConne
41
47
  const ConnectorComponent = connectorMap[getConnectorKey()]
42
48
  if (!ConnectorComponent) return null
43
49
 
44
- return <View style={styles.theirReplyConnectorContainer}>{ConnectorComponent}</View>
50
+ const spacerStyle = nextRendersAuthor ? styles.theirReplyConnectorSpacer : null
51
+
52
+ return (
53
+ <View style={[styles.theirReplyConnectorContainer, spacerStyle]}>{ConnectorComponent}</View>
54
+ )
45
55
  }
46
56
 
47
57
  export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnectorProps) {
48
58
  const styles = useStyles()
59
+ const { nextRendersAuthor, threadPosition, prevIsMyReply, nextIsMyReply, isReplyShadowMessage } =
60
+ message
49
61
 
50
62
  if (!REPLIES_FEATURE_ENABLED) return null
63
+ if (messageBubbleHeight === 0) return null // Prevents UI shifting
51
64
 
52
65
  const connectorMap: Record<string, React.ReactNode | null> = {
53
66
  longTailCurve: (
@@ -63,14 +76,12 @@ export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnecto
63
76
  />
64
77
  ),
65
78
  verticalLine: <VerticalLineConnector style={styles.myReplyConnectorPosition} />,
66
- swirl: <SwirlConnector style={styles.myReplyConnectorPosition} />,
79
+ swirl: <SwirlConnector style={styles.myReplyConnectorPosition} height={messageBubbleHeight} />,
67
80
  default: null,
68
81
  }
69
82
 
70
83
  const getConnectorKey = () => {
71
- const { threadPosition, prevIsMyReply, nextIsMyReply } = message
72
-
73
- if (threadPosition === 'first') return 'longHeadCurve'
84
+ if (threadPosition === 'first' || isReplyShadowMessage) return 'longHeadCurve'
74
85
  if (threadPosition === 'center' && prevIsMyReply && nextIsMyReply) return 'verticalLine'
75
86
  if (threadPosition === 'center' && (!prevIsMyReply || !nextIsMyReply)) return 'swirl'
76
87
  if (threadPosition === 'last') return 'longTailCurve'
@@ -81,7 +92,9 @@ export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnecto
81
92
  const ConnectorComponent = connectorMap[getConnectorKey()]
82
93
  if (!ConnectorComponent) return null
83
94
 
84
- return <View style={styles.myReplyConnectorContainer}>{ConnectorComponent}</View>
95
+ const spacerStyle = nextRendersAuthor ? styles.myReplyConnectorSpacer : null
96
+
97
+ return <View style={[styles.myReplyConnectorContainer, spacerStyle]}>{ConnectorComponent}</View>
85
98
  }
86
99
 
87
100
  function VerticalLineConnector({ style }: { style?: ViewStyle }) {
@@ -109,21 +122,23 @@ function ShortTailCurveConnector({ height }: { height: number }) {
109
122
  return <View style={[styles.shortTailCurveConnector, { height }]} />
110
123
  }
111
124
 
112
- function SwirlConnector({ style }: { style?: ViewStyle }) {
125
+ function SwirlConnector({ style, height }: { style?: ViewStyle; height: number }) {
113
126
  const styles = useStyles()
114
127
  const { colors } = useTheme()
115
- const borderColor = colors.borderColorDefaultBase
116
128
 
117
129
  return (
118
130
  <View style={[styles.swirlConnectorContainer, style]}>
119
- <View style={styles.swirlConnectorVerticalLineHead} />
120
- <Svg width={27} height={34} fill="none">
121
- <Path
122
- stroke={borderColor}
123
- strokeWidth={CONNECTOR_BORDER_WIDTH}
124
- d="M2.07 34c0-6.142 1.201-12.283 3.343-17m0 0c2.305-5.075 5.699-8.5 9.857-8.5 4.86 0 8.8 3.806 8.8 8.5s-3.94 8.5-8.8 8.5c-4.158 0-7.552-3.425-9.857-8.5zm0 0C3.271 12.283 2.07 6.142 2.07 0"
125
- />
126
- </Svg>
131
+ <View style={{ height }}>
132
+ <View style={styles.swirlConnectorVerticalLineHead} />
133
+ <Svg width={27} height={34} fill="none">
134
+ <Path
135
+ stroke={colors.borderColorDefaultBase}
136
+ strokeWidth={CONNECTOR_BORDER_WIDTH}
137
+ d="M2.07 34c0-6.142 1.201-12.283 3.343-17m0 0c2.305-5.075 5.699-8.5 9.857-8.5 4.86 0 8.8 3.806 8.8 8.5s-3.94 8.5-8.8 8.5c-4.158 0-7.552-3.425-9.857-8.5zm0 0C3.271 12.283 2.07 6.142 2.07 0"
138
+ />
139
+ </Svg>
140
+ <View style={styles.swirlConnectorVerticalLineTail} />
141
+ </View>
127
142
  <View style={styles.swirlConnectorVerticalLineTail} />
128
143
  </View>
129
144
  )
@@ -136,19 +151,26 @@ const useStyles = () => {
136
151
  return StyleSheet.create({
137
152
  theirReplyConnectorContainer: {
138
153
  width: '50%',
154
+ marginRight: OFFSET_CONNECTOR_CONTAINER,
139
155
  alignSelf: 'flex-end',
140
156
  flex: 1,
141
157
  },
158
+ theirReplyConnectorSpacer: {
159
+ paddingBottom: AVATAR_CONNECTOR_SPACING,
160
+ },
142
161
  myReplyConnectorContainer: {
143
162
  width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
144
163
  position: 'absolute',
145
- left: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
164
+ left: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL - OFFSET_CONNECTOR_CONTAINER,
146
165
  top: 0,
147
166
  bottom: 0,
148
167
  },
149
168
  myReplyConnectorPosition: {
150
169
  left: '50%',
151
170
  },
171
+ myReplyConnectorSpacer: {
172
+ bottom: AVATAR_CONNECTOR_SPACING,
173
+ },
152
174
  verticalLineConnector: {
153
175
  flex: 1,
154
176
  borderLeftWidth: CONNECTOR_BORDER_WIDTH,
@@ -22,14 +22,45 @@ import {
22
22
  } from '../../utils/styles'
23
23
  import Animated from 'react-native-reanimated'
24
24
  import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
25
- import { assertKeysAreNumbers } from '../../utils'
25
+ import { assertKeysAreNumbers, pluralize } from '../../utils'
26
26
  import { useNavigation } from '@react-navigation/native'
27
+ import { useConversationMessage } from '../../hooks/use_conversation_message'
27
28
 
28
- interface ShadowMessageProps extends MessageResource {
29
+ interface ReplyShadowMessageProps extends MessageResource {
30
+ messageId: string
29
31
  conversation_id: number
32
+ inReplyScreen?: boolean
33
+ isReplyShadowMessage: boolean
34
+ nextRendersAuthor: boolean
30
35
  }
31
36
 
32
- export function ShadowMessage({ conversation_id, ...message }: ShadowMessageProps) {
37
+ export function ReplyShadowMessage({
38
+ conversation_id,
39
+ inReplyScreen,
40
+ messageId,
41
+ isReplyShadowMessage,
42
+ nextRendersAuthor,
43
+ }: ReplyShadowMessageProps) {
44
+ const { message, isError, isLoading } = useConversationMessage({ conversation_id, messageId })
45
+
46
+ if (inReplyScreen) return null
47
+ if (isLoading) return <ShadowMessageFallback text="Loading..." />
48
+ if (isError || !message) return <ShadowMessageFallback text="Message not found" />
49
+
50
+ const enrichedMessage = {
51
+ ...message,
52
+ isReplyShadowMessage,
53
+ nextRendersAuthor,
54
+ }
55
+
56
+ return <ShadowMessageContent conversation_id={conversation_id} {...enrichedMessage} />
57
+ }
58
+
59
+ interface ShadowMessageContentProps extends MessageResource {
60
+ conversation_id: number
61
+ }
62
+
63
+ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageContentProps) {
33
64
  const { text } = message
34
65
  const styles = useStyles(message)
35
66
  const { colors } = useTheme()
@@ -39,12 +70,13 @@ export function ShadowMessage({ conversation_id, ...message }: ShadowMessageProp
39
70
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
40
71
  useAnimatedMessageBackgroundColor()
41
72
  const scalableNumberOfLines = useScalableNumberOfLines(2)
73
+ const replyCountText = pluralize(message.replyCount, 'reply')
42
74
 
43
75
  const handleNavigateToReplies = () => {
44
76
  navigation.navigate('ConversationReply', {
45
77
  conversation_id,
46
- reply_root_id: message.id,
47
- // TODO: Add a way to pass the reply root author's name
78
+ reply_root_id: message.replyRootId,
79
+ reply_root_author_name: message.author.name,
48
80
  })
49
81
  }
50
82
 
@@ -89,7 +121,7 @@ export function ShadowMessage({ conversation_id, ...message }: ShadowMessageProp
89
121
  </View>
90
122
  <View style={styles.messageMeta}>
91
123
  <Text variant="footnote" style={styles.replyCountText}>
92
- {message.replyCount} replies
124
+ {replyCountText}
93
125
  </Text>
94
126
  </View>
95
127
  </View>
@@ -203,6 +235,23 @@ function MessageAttachmentIcon({ iconName }: { iconName: IconProps['name'] }) {
203
235
  )
204
236
  }
205
237
 
238
+ // TODO: The mine prop is not used yet, but in the future we will be adding a way to find the `mine` value for this fallback.
239
+ function ShadowMessageFallback({ text, mine }: { text: string; mine?: boolean }) {
240
+ const styles = useStyles({ mine })
241
+
242
+ return (
243
+ <View style={styles.message}>
244
+ <View style={styles.messageContent}>
245
+ <View style={styles.messageBubble}>
246
+ <Text variant="footnote" style={styles.messageText}>
247
+ {text}
248
+ </Text>
249
+ </View>
250
+ </View>
251
+ </View>
252
+ )
253
+ }
254
+
206
255
  interface StylesProps {
207
256
  imageWidth?: number
208
257
  imageHeight?: number
@@ -0,0 +1,25 @@
1
+ import { MessageResource } from '../types'
2
+ import { useApiGet } from './use_api'
3
+ import { getMessageRequestArgs, getMessageQueryKey } from '../utils/request/get_message'
4
+
5
+ export const useConversationMessage = ({
6
+ conversation_id,
7
+ messageId,
8
+ enabled,
9
+ }: {
10
+ conversation_id: number
11
+ messageId: string
12
+ enabled?: boolean
13
+ }) => {
14
+ const {
15
+ data: message,
16
+ isError,
17
+ isLoading,
18
+ } = useApiGet<MessageResource>({
19
+ ...getMessageRequestArgs({ conversation_id, messageId }),
20
+ enabled,
21
+ })
22
+ const queryKey = getMessageQueryKey({ conversation_id, messageId })
23
+
24
+ return { message, isError, isLoading, queryKey }
25
+ }
@@ -1,13 +1,10 @@
1
1
  import { useMemo } from 'react'
2
2
  import { MessageResource } from '../types'
3
- import {
4
- getRequestQueryKey,
5
- SuspensePaginatorOptions,
6
- useSuspensePaginator,
7
- } from './use_suspense_api'
3
+ import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
4
+ import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
8
5
 
9
6
  export const useConversationMessages = (
10
- { conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string },
7
+ { conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string | null },
11
8
  opts?: SuspensePaginatorOptions
12
9
  ) => {
13
10
  const { data, refetch, isRefetching, fetchNextPage } = useSuspensePaginator<MessageResource>(
@@ -27,50 +24,3 @@ export const useConversationMessages = (
27
24
 
28
25
  return { messages, refetch, isRefetching, fetchNextPage, queryKey }
29
26
  }
30
-
31
- export const getMessagesRequestArgs = ({
32
- conversation_id,
33
- reply_root_id,
34
- }: {
35
- conversation_id: number
36
- reply_root_id?: string
37
- }) => {
38
- const url = reply_root_id
39
- ? `/me/conversations/${conversation_id}/messages/${reply_root_id}/replies`
40
- : `/me/conversations/${conversation_id}/messages`
41
-
42
- return {
43
- url,
44
- data: {
45
- perPage: 25,
46
- fields: {
47
- Message: [
48
- 'text',
49
- 'text_edited_at',
50
- 'mine',
51
- 'attachments',
52
- 'created_at',
53
- 'deleted_at',
54
- 'author',
55
- 'reaction_counts',
56
- 'reply_count',
57
- 'reply_root',
58
- ],
59
- Person: ['name', 'avatar'],
60
- ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],
61
- },
62
- include: ['author', 'reaction_counts'],
63
- },
64
- }
65
- }
66
-
67
- export const getMessagesQueryKey = ({
68
- conversation_id,
69
- reply_root_id,
70
- }: {
71
- conversation_id: number
72
- reply_root_id?: string
73
- }) => {
74
- const requestArgs = getMessagesRequestArgs({ conversation_id, reply_root_id })
75
- return getRequestQueryKey(requestArgs)
76
- }
@@ -12,7 +12,7 @@ import { transformMessageEventDataToMessageResource } from '../utils/jolt/transf
12
12
  import { getRequestQueryKey } from './use_suspense_api'
13
13
  import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'
14
14
  import { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'
15
- import { getMessagesRequestArgs } from '../utils/request/messages'
15
+ import { getMessagesRequestArgs } from '../utils/request/get_messages'
16
16
  import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
17
17
  import { isTemporaryMessageId } from './use_message_create_or_update'
18
18
  import { completeMessageCreationTracking } from '../utils/performance_tracking'
@@ -1,5 +1,5 @@
1
1
  import { InfiniteData, useMutation } from '@tanstack/react-query'
2
- import { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_messages'
2
+ import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
3
3
  import { useApiClient } from './use_api_client'
4
4
  import { ApiCollection, ApiResource, MessageResource } from '../types'
5
5
  import { chatQueryClient } from '../contexts/api_provider'
@@ -18,7 +18,7 @@ import { startMessageCreationTracking } from '../utils/performance_tracking'
18
18
  interface Props {
19
19
  conversationId: number
20
20
  message?: MessageResource
21
- replyRootId?: string
21
+ replyRootId?: string | null
22
22
  }
23
23
 
24
24
  export function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {
@@ -2,7 +2,7 @@ import { InfiniteData, useMutation, useQueryClient } from '@tanstack/react-query
2
2
  import { useCallback } from 'react'
3
3
  import { Alert } from 'react-native'
4
4
  import { useApiClient } from './use_api_client'
5
- import { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_messages'
5
+ import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
6
6
  import { ApiCollection, ApiResource, MessageResource } from '../types'
7
7
  import { ReactionCountResource } from '../types/resources/reaction'
8
8
  import { updateRecordInPagesData } from '../utils'
@@ -187,9 +187,9 @@ export const ChatStack = createNativeStackNavigator({
187
187
  },
188
188
  ConversationReply: {
189
189
  screen: ConversationScreen,
190
- options: {
191
- title: 'Reply', // TODO: Get root reply author
192
- },
190
+ options: ({ route }) => ({
191
+ title: (route.params as ConversationRouteProps)?.title ?? 'Reply',
192
+ }),
193
193
  },
194
194
  TeamConversation: {
195
195
  screen: TeamConversationScreen,
@@ -34,11 +34,13 @@ import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
34
34
  import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
35
35
  import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
36
36
  import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
37
+ import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
38
+ import { REPLIES_FEATURE_ENABLED } from '../utils'
37
39
 
38
40
  export type ConversationRouteProps = {
39
41
  conversation_id: number
40
- reply_root_id?: string
41
- replyRootAuthor?: string
42
+ reply_root_id?: string | null
43
+ reply_root_author_name?: string
42
44
  chat_group_graph_id?: string
43
45
  clear_input?: boolean
44
46
  editing_message_id?: number | null
@@ -53,7 +55,8 @@ export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
53
55
  export function ConversationScreen({ route }: ConversationScreenProps) {
54
56
  const styles = useStyles()
55
57
  const navigation = useNavigation()
56
- const { conversation_id, editing_message_id, reply_root_id } = route.params
58
+ const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
59
+ route.params
57
60
  const { data: conversation } = useConversation(route.params)
58
61
  const { messages, refetch, isRefetching, fetchNextPage } = useConversationMessages({
59
62
  conversation_id,
@@ -71,6 +74,10 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
71
74
  const showLeaderDisabledReplyBanner = canReply && repliesDisabled
72
75
  const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
73
76
  const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))
77
+ const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]
78
+ const replyHeaderTitle = replyRootAuthorFirstName
79
+ ? `Reply to ${replyRootAuthorFirstName}`
80
+ : 'Reply'
74
81
 
75
82
  const listRef = useRef<FlatList>(null)
76
83
  const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
@@ -87,14 +94,18 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
87
94
  }, [])
88
95
 
89
96
  useEffect(() => {
90
- if (reply_root_id) return
91
-
92
- navigation.setParams({
93
- title: title,
94
- badge: badges?.[0],
95
- deleted: conversation?.deleted,
96
- })
97
- }, [navigation, title, badges, conversation?.deleted, reply_root_id])
97
+ if (reply_root_id) {
98
+ navigation.setParams({
99
+ title: replyHeaderTitle,
100
+ })
101
+ } else {
102
+ navigation.setParams({
103
+ title: title,
104
+ badge: badges?.[0],
105
+ deleted: conversation?.deleted,
106
+ })
107
+ }
108
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle])
98
109
 
99
110
  if (!conversation || conversation.deleted) {
100
111
  return (
@@ -136,6 +147,16 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
136
147
  return <InlineDateSeparator {...item} />
137
148
  }
138
149
 
150
+ if (item.type === 'ReplyShadowMessage') {
151
+ return (
152
+ <ReplyShadowMessage
153
+ {...item}
154
+ conversation_id={conversation_id}
155
+ inReplyScreen={!!reply_root_id}
156
+ />
157
+ )
158
+ }
159
+
139
160
  return (
140
161
  <Message
141
162
  {...item}
@@ -155,6 +176,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
155
176
  {showLeaderDisabledReplyBanner && <LeaderDisabledRepliesBanner />}
156
177
  {canReply ? (
157
178
  <MessageForm.Root
179
+ replyRootAuthorFirstName={replyRootAuthorFirstName}
158
180
  conversation={conversation}
159
181
  replyRootId={reply_root_id}
160
182
  currentlyEditingMessage={currentlyEditingMessage}
@@ -219,13 +241,21 @@ const useDateSeparatorStyles = () => {
219
241
  })
220
242
  }
221
243
 
244
+ type ReplyShadowMessage = {
245
+ type: 'ReplyShadowMessage'
246
+ id: string
247
+ messageId: string
248
+ isReplyShadowMessage: boolean
249
+ nextRendersAuthor: boolean
250
+ }
251
+
222
252
  interface GroupMessagesProps {
223
253
  ms: MessageResource[]
224
254
  inReplyScreen?: boolean
225
255
  }
226
256
 
227
257
  export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
228
- let enrichedMessages: (MessageResource | DateSeparator)[] = []
258
+ let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
229
259
  let encounteredOneOfMyMessages = false
230
260
 
231
261
  ms.forEach((message, i) => {
@@ -233,7 +263,9 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
233
263
  const nextMessage = ms[i - 1]
234
264
  const date = moment(message.createdAt).format('YYYY-MM-DD')
235
265
  const inThread = message.replyRootId !== null
266
+ const nextMessageInThread = nextMessage?.replyRootId !== null
236
267
  const threadRoot = message.replyRootId === message.id
268
+ const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id
237
269
  const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId
238
270
  const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId
239
271
  const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id
@@ -244,8 +276,14 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
244
276
  const nextMessageMoreThan5Minutes =
245
277
  nextMessage &&
246
278
  new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5
279
+ const prevMessageIsDateSeparator =
280
+ prevMessage && date !== moment(prevMessage.createdAt).format('YYYY-MM-DD')
247
281
  const nextMessageIsDateSeparator =
248
282
  nextMessage && date !== moment(nextMessage.createdAt).format('YYYY-MM-DD')
283
+ const insertReplyShadowMessage =
284
+ message.replyRootId &&
285
+ !threadRoot &&
286
+ (prevMessageDifferentThread || prevMessageIsDateSeparator)
249
287
 
250
288
  if (message.mine && !encounteredOneOfMyMessages) {
251
289
  encounteredOneOfMyMessages = true
@@ -257,6 +295,13 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
257
295
  message.renderAuthor =
258
296
  !message.mine && (!prevMessage || prevMessageDifferentAuthor || prevMessageMoreThan5Minutes)
259
297
  message.threadPosition = null
298
+ message.nextRendersAuthor = nextMessage?.renderAuthor
299
+ message.isReplyShadowMessage = false
300
+ message.nextIsReplyShadowMessage =
301
+ REPLIES_FEATURE_ENABLED &&
302
+ nextMessageInThread &&
303
+ !nextMessageThreadRoot &&
304
+ (nextMessageDifferentThread || nextMessageIsDateSeparator)
260
305
 
261
306
  if (!inReplyScreen && inThread) {
262
307
  message.prevIsMyReply = prevMessage?.mine
@@ -274,6 +319,17 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
274
319
  }
275
320
 
276
321
  enrichedMessages.push(message)
322
+
323
+ if (insertReplyShadowMessage && REPLIES_FEATURE_ENABLED) {
324
+ enrichedMessages.push({
325
+ type: 'ReplyShadowMessage',
326
+ id: `${message.id}-${message.replyRootId}`,
327
+ messageId: message.replyRootId!,
328
+ isReplyShadowMessage: true,
329
+ nextRendersAuthor: message?.renderAuthor,
330
+ })
331
+ }
332
+
277
333
  if (!prevMessage || date !== moment(prevMessage.createdAt).format('YYYY-MM-DD')) {
278
334
  enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
279
335
  }
@@ -23,13 +23,20 @@ export const MessageActionsScreenOptions = getFormSheetScreenOptions({
23
23
 
24
24
  export type MessageActionsScreenProps = StaticScreenProps<{
25
25
  message_id: string
26
+ reply_root_author_name?: string
26
27
  conversation_id: number
27
28
  canDeleteNonAuthoredMessages?: boolean
28
29
  inReplyScreen?: boolean
29
30
  }>
30
31
 
31
32
  export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
32
- const { conversation_id, message_id, canDeleteNonAuthoredMessages, inReplyScreen } = route.params
33
+ const {
34
+ conversation_id,
35
+ message_id,
36
+ canDeleteNonAuthoredMessages,
37
+ inReplyScreen,
38
+ reply_root_author_name,
39
+ } = route.params
33
40
 
34
41
  const { messages, refetch } = useConversationMessages(
35
42
  { conversation_id },
@@ -46,6 +53,7 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
46
53
  canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages || false}
47
54
  refetchMessages={refetch}
48
55
  inReplyScreen={inReplyScreen}
56
+ replyRootAuthorName={reply_root_author_name}
49
57
  />
50
58
  )
51
59
  }
@@ -56,12 +64,14 @@ function MessageActionsScreenContent({
56
64
  canDeleteNonAuthoredMessages,
57
65
  refetchMessages,
58
66
  inReplyScreen,
67
+ replyRootAuthorName,
59
68
  }: {
60
69
  message: MessageResource
61
70
  conversation_id: number
62
71
  canDeleteNonAuthoredMessages: boolean
63
72
  refetchMessages: () => void
64
73
  inReplyScreen?: boolean
74
+ replyRootAuthorName?: string
65
75
  }) {
66
76
  const navigation = useNavigation()
67
77
  const apiClient = useApiClient()
@@ -95,10 +105,10 @@ function MessageActionsScreenContent({
95
105
  navigation.navigate('ConversationReply', {
96
106
  conversation_id,
97
107
  reply_root_id: message.replyRootId || message.id,
98
- // TODO: Update title param with reply root author
108
+ reply_root_author_name: replyRootAuthorName,
99
109
  })
100
110
  })
101
- }, [navigation, conversation_id, message.id, message.replyRootId])
111
+ }, [navigation, conversation_id, message.id, message.replyRootId, replyRootAuthorName])
102
112
 
103
113
  const handleCopyPress = () => {
104
114
  Clipboard.setStringAsync(message?.text || attachmentForCopy() || '')
@@ -26,6 +26,9 @@ export interface MessageResource {
26
26
  threadPosition?: 'first' | 'center' | 'last' | null
27
27
  prevIsMyReply?: boolean
28
28
  nextIsMyReply?: boolean
29
+ nextRendersAuthor?: boolean
30
+ isReplyShadowMessage?: boolean
31
+ nextIsReplyShadowMessage?: boolean
29
32
 
30
33
  // Properties for mutation state
31
34
  pending?: boolean // Indicates if the message is optimistically created and pending server response
@@ -1,6 +1,6 @@
1
1
  import { InfiniteData } from '@tanstack/react-query'
2
2
  import { ApiCollection, CurrentPersonResource, MessageResource } from '../../types'
3
- import { getMessagesQueryKey } from '../../hooks/use_conversation_messages'
3
+ import { getMessagesQueryKey } from '../../utils/request/get_messages'
4
4
  import { chatQueryClient } from '../../contexts/api_provider'
5
5
  import { updateOrCreateRecordInPagesData } from './page_mutations'
6
6
  import { convertAttachmentsForCreate } from '../convert_attachments_for_create'
@@ -1,6 +1,6 @@
1
1
  import { InfiniteData } from '@tanstack/react-query'
2
2
  import { ApiCollection, MessageResource } from '../../types'
3
- import { getMessagesQueryKey } from '../../hooks/use_conversation_messages'
3
+ import { getMessagesQueryKey } from '../../utils/request/get_messages'
4
4
  import { chatQueryClient } from '../../contexts/api_provider'
5
5
  import { updateOrCreateRecordInPagesData } from './page_mutations'
6
6
 
@@ -1,4 +1,4 @@
1
- const irregularInflections: Record<string, string> = { person: 'people' }
1
+ const irregularInflections: Record<string, string> = { person: 'people', reply: 'replies' }
2
2
 
3
3
  export function pluralize(count: number, singularWord: string, includeCount = true) {
4
4
  const plural = count !== 1