@planningcenter/chat-react-native 3.17.0-rc.0 → 3.17.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +16 -6
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_form.d.ts +1 -1
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js.map +1 -1
  7. package/build/components/conversation/reply_connectors.d.ts +1 -0
  8. package/build/components/conversation/reply_connectors.d.ts.map +1 -1
  9. package/build/components/conversation/reply_connectors.js +33 -14
  10. package/build/components/conversation/reply_connectors.js.map +1 -1
  11. package/build/components/conversation/reply_shadow_message.d.ts +12 -0
  12. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -0
  13. package/build/components/conversation/{shadow_message.js → reply_shadow_message.js} +35 -5
  14. package/build/components/conversation/reply_shadow_message.js.map +1 -0
  15. package/build/hooks/use_conversation_message.d.ts +11 -0
  16. package/build/hooks/use_conversation_message.d.ts.map +1 -0
  17. package/build/hooks/use_conversation_message.js +8 -0
  18. package/build/hooks/use_conversation_message.js.map +1 -0
  19. package/build/hooks/use_conversation_messages.d.ts +1 -20
  20. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  21. package/build/hooks/use_conversation_messages.js +2 -33
  22. package/build/hooks/use_conversation_messages.js.map +1 -1
  23. package/build/hooks/use_conversation_messages_jolt_events.js +1 -1
  24. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  25. package/build/hooks/use_message_create_or_update.d.ts +1 -1
  26. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  27. package/build/hooks/use_message_create_or_update.js +1 -1
  28. package/build/hooks/use_message_create_or_update.js.map +1 -1
  29. package/build/hooks/use_message_reaction_toggle.js +1 -1
  30. package/build/hooks/use_message_reaction_toggle.js.map +1 -1
  31. package/build/screens/conversation_screen.d.ts +10 -2
  32. package/build/screens/conversation_screen.d.ts.map +1 -1
  33. package/build/screens/conversation_screen.js +27 -0
  34. package/build/screens/conversation_screen.js.map +1 -1
  35. package/build/types/resources/message.d.ts +3 -0
  36. package/build/types/resources/message.d.ts.map +1 -1
  37. package/build/types/resources/message.js.map +1 -1
  38. package/build/utils/cache/optimistically_create_message.js +1 -1
  39. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  40. package/build/utils/cache/optimistically_update_message.js +1 -1
  41. package/build/utils/cache/optimistically_update_message.js.map +1 -1
  42. package/build/utils/pluralize.js +1 -1
  43. package/build/utils/pluralize.js.map +1 -1
  44. package/build/utils/request/get_message.d.ts +20 -0
  45. package/build/utils/request/get_message.d.ts.map +1 -0
  46. package/build/utils/request/get_message.js +18 -0
  47. package/build/utils/request/get_message.js.map +1 -0
  48. package/build/utils/request/get_messages.d.ts +20 -0
  49. package/build/utils/request/get_messages.d.ts.map +1 -0
  50. package/build/utils/request/get_messages.js +20 -0
  51. package/build/utils/request/get_messages.js.map +1 -0
  52. package/build/utils/request/messages_data_options.d.ts +7 -0
  53. package/build/utils/request/messages_data_options.d.ts.map +1 -0
  54. package/build/utils/request/messages_data_options.js +18 -0
  55. package/build/utils/request/messages_data_options.js.map +1 -0
  56. package/package.json +2 -2
  57. package/src/__tests__/utils/pluralize.tsx +3 -0
  58. package/src/components/conversation/message.tsx +19 -6
  59. package/src/components/conversation/message_form.tsx +2 -2
  60. package/src/components/conversation/reply_connectors.tsx +42 -20
  61. package/src/components/conversation/{shadow_message.tsx → reply_shadow_message.tsx} +54 -5
  62. package/src/hooks/use_conversation_message.ts +20 -0
  63. package/src/hooks/use_conversation_messages.ts +3 -53
  64. package/src/hooks/use_conversation_messages_jolt_events.ts +1 -1
  65. package/src/hooks/use_message_create_or_update.ts +2 -2
  66. package/src/hooks/use_message_reaction_toggle.ts +1 -1
  67. package/src/screens/conversation_screen.tsx +48 -2
  68. package/src/types/resources/message.ts +3 -0
  69. package/src/utils/cache/optimistically_create_message.ts +1 -1
  70. package/src/utils/cache/optimistically_update_message.ts +1 -1
  71. package/src/utils/pluralize.ts +1 -1
  72. package/src/utils/request/get_message.ts +32 -0
  73. package/src/utils/request/get_messages.ts +34 -0
  74. package/src/utils/request/messages_data_options.ts +18 -0
  75. package/build/components/conversation/shadow_message.d.ts +0 -8
  76. package/build/components/conversation/shadow_message.d.ts.map +0 -1
  77. package/build/components/conversation/shadow_message.js.map +0 -1
  78. package/build/utils/request/messages.d.ts +0 -15
  79. package/build/utils/request/messages.d.ts.map +0 -1
  80. package/build/utils/request/messages.js +0 -22
  81. package/build/utils/request/messages.js.map +0 -1
  82. 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,11 +70,12 @@ 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,
78
+ reply_root_id: message.replyRootId,
47
79
  // TODO: Add a way to pass the reply root author's name
48
80
  })
49
81
  }
@@ -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,20 @@
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
+ }: {
9
+ conversation_id: number
10
+ messageId: string
11
+ }) => {
12
+ const {
13
+ data: message,
14
+ isError,
15
+ isLoading,
16
+ } = useApiGet<MessageResource>(getMessageRequestArgs({ conversation_id, messageId }))
17
+ const queryKey = getMessageQueryKey({ conversation_id, messageId })
18
+
19
+ return { message, isError, isLoading, queryKey }
20
+ }
@@ -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'
@@ -34,10 +34,12 @@ 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
42
+ reply_root_id?: string | null
41
43
  replyRootAuthor?: string
42
44
  chat_group_graph_id?: string
43
45
  clear_input?: boolean
@@ -136,6 +138,16 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
136
138
  return <InlineDateSeparator {...item} />
137
139
  }
138
140
 
141
+ if (item.type === 'ReplyShadowMessage') {
142
+ return (
143
+ <ReplyShadowMessage
144
+ {...item}
145
+ conversation_id={conversation_id}
146
+ inReplyScreen={!!reply_root_id}
147
+ />
148
+ )
149
+ }
150
+
139
151
  return (
140
152
  <Message
141
153
  {...item}
@@ -219,13 +231,21 @@ const useDateSeparatorStyles = () => {
219
231
  })
220
232
  }
221
233
 
234
+ type ReplyShadowMessage = {
235
+ type: 'ReplyShadowMessage'
236
+ id: string
237
+ messageId: string
238
+ isReplyShadowMessage: boolean
239
+ nextRendersAuthor: boolean
240
+ }
241
+
222
242
  interface GroupMessagesProps {
223
243
  ms: MessageResource[]
224
244
  inReplyScreen?: boolean
225
245
  }
226
246
 
227
247
  export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
228
- let enrichedMessages: (MessageResource | DateSeparator)[] = []
248
+ let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
229
249
  let encounteredOneOfMyMessages = false
230
250
 
231
251
  ms.forEach((message, i) => {
@@ -233,7 +253,9 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
233
253
  const nextMessage = ms[i - 1]
234
254
  const date = moment(message.createdAt).format('YYYY-MM-DD')
235
255
  const inThread = message.replyRootId !== null
256
+ const nextMessageInThread = nextMessage?.replyRootId !== null
236
257
  const threadRoot = message.replyRootId === message.id
258
+ const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id
237
259
  const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId
238
260
  const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId
239
261
  const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id
@@ -244,8 +266,14 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
244
266
  const nextMessageMoreThan5Minutes =
245
267
  nextMessage &&
246
268
  new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5
269
+ const prevMessageIsDateSeparator =
270
+ prevMessage && date !== moment(prevMessage.createdAt).format('YYYY-MM-DD')
247
271
  const nextMessageIsDateSeparator =
248
272
  nextMessage && date !== moment(nextMessage.createdAt).format('YYYY-MM-DD')
273
+ const insertReplyShadowMessage =
274
+ message.replyRootId &&
275
+ !threadRoot &&
276
+ (prevMessageDifferentThread || prevMessageIsDateSeparator)
249
277
 
250
278
  if (message.mine && !encounteredOneOfMyMessages) {
251
279
  encounteredOneOfMyMessages = true
@@ -257,6 +285,13 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
257
285
  message.renderAuthor =
258
286
  !message.mine && (!prevMessage || prevMessageDifferentAuthor || prevMessageMoreThan5Minutes)
259
287
  message.threadPosition = null
288
+ message.nextRendersAuthor = nextMessage?.renderAuthor
289
+ message.isReplyShadowMessage = false
290
+ message.nextIsReplyShadowMessage =
291
+ REPLIES_FEATURE_ENABLED &&
292
+ nextMessageInThread &&
293
+ !nextMessageThreadRoot &&
294
+ (nextMessageDifferentThread || nextMessageIsDateSeparator)
260
295
 
261
296
  if (!inReplyScreen && inThread) {
262
297
  message.prevIsMyReply = prevMessage?.mine
@@ -274,6 +309,17 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
274
309
  }
275
310
 
276
311
  enrichedMessages.push(message)
312
+
313
+ if (insertReplyShadowMessage && REPLIES_FEATURE_ENABLED) {
314
+ enrichedMessages.push({
315
+ type: 'ReplyShadowMessage',
316
+ id: `${message.id}-${message.replyRootId}`,
317
+ messageId: message.replyRootId!,
318
+ isReplyShadowMessage: true,
319
+ nextRendersAuthor: message?.renderAuthor,
320
+ })
321
+ }
322
+
277
323
  if (!prevMessage || date !== moment(prevMessage.createdAt).format('YYYY-MM-DD')) {
278
324
  enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
279
325
  }
@@ -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
@@ -0,0 +1,32 @@
1
+ import { getRequestQueryKey } from '../../hooks/use_suspense_api'
2
+ import { getMessageFields, getMessagesInclude } from './messages_data_options'
3
+
4
+ export const getMessageRequestArgs = ({
5
+ conversation_id,
6
+ messageId,
7
+ }: {
8
+ conversation_id: number
9
+ messageId: string
10
+ }) => {
11
+ const url = `/me/conversations/${conversation_id}/messages/${messageId}`
12
+
13
+ return {
14
+ url,
15
+ data: {
16
+ perPage: 25,
17
+ fields: getMessageFields,
18
+ include: getMessagesInclude,
19
+ },
20
+ }
21
+ }
22
+
23
+ export const getMessageQueryKey = ({
24
+ conversation_id,
25
+ messageId,
26
+ }: {
27
+ conversation_id: number
28
+ messageId: string
29
+ }) => {
30
+ const requestArgs = getMessageRequestArgs({ conversation_id, messageId })
31
+ return getRequestQueryKey(requestArgs)
32
+ }
@@ -0,0 +1,34 @@
1
+ import { getRequestQueryKey } from '../../hooks/use_suspense_api'
2
+ import { getMessageFields, getMessagesInclude } from './messages_data_options'
3
+
4
+ export const getMessagesRequestArgs = ({
5
+ conversation_id,
6
+ reply_root_id,
7
+ }: {
8
+ conversation_id: number
9
+ reply_root_id?: string | null
10
+ }) => {
11
+ const url = reply_root_id
12
+ ? `/me/conversations/${conversation_id}/messages/${reply_root_id}/replies`
13
+ : `/me/conversations/${conversation_id}/messages`
14
+
15
+ return {
16
+ url,
17
+ data: {
18
+ perPage: 25,
19
+ fields: getMessageFields,
20
+ include: getMessagesInclude,
21
+ },
22
+ }
23
+ }
24
+
25
+ export const getMessagesQueryKey = ({
26
+ conversation_id,
27
+ reply_root_id,
28
+ }: {
29
+ conversation_id: number
30
+ reply_root_id?: string | null
31
+ }) => {
32
+ const requestArgs = getMessagesRequestArgs({ conversation_id, reply_root_id })
33
+ return getRequestQueryKey(requestArgs)
34
+ }
@@ -0,0 +1,18 @@
1
+ export const getMessageFields = {
2
+ Message: [
3
+ 'text',
4
+ 'text_edited_at',
5
+ 'mine',
6
+ 'attachments',
7
+ 'created_at',
8
+ 'deleted_at',
9
+ 'author',
10
+ 'reaction_counts',
11
+ 'reply_count',
12
+ 'reply_root',
13
+ ],
14
+ Person: ['name', 'avatar'],
15
+ ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],
16
+ }
17
+
18
+ export const getMessagesInclude = ['author', 'reaction_counts']
@@ -1,8 +0,0 @@
1
- import React from 'react';
2
- import { MessageResource } from '../../types';
3
- interface ShadowMessageProps extends MessageResource {
4
- conversation_id: number;
5
- }
6
- export declare function ShadowMessage({ conversation_id, ...message }: ShadowMessageProps): React.JSX.Element;
7
- export {};
8
- //# sourceMappingURL=shadow_message.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shadow_message.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/shadow_message.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAkB7C,UAAU,kBAAmB,SAAQ,eAAe;IAClD,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,wBAAgB,aAAa,CAAC,EAAE,eAAe,EAAE,GAAG,OAAO,EAAE,EAAE,kBAAkB,qBAsEhF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"shadow_message.js","sourceRoot":"","sources":["../../../src/components/conversation/shadow_message.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react'\nimport { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'\nimport { Avatar, Icon, IconProps, Image, Text } from '../display'\nimport {\n useAnimatedMessageBackgroundColor,\n useFontScale,\n useScalableNumberOfLines,\n useTheme,\n} from '../../hooks'\nimport { MessageResource } from '../../types'\nimport {\n DenormalizedAttachmentResource,\n DenormalizedGiphyAttachmentResource,\n DenormalizedExpandedLinkAttachmentResource,\n DenormalizedMessageAttachmentResource,\n} from '../../types/resources/denormalized_attachment_resource'\nimport {\n CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,\n MAX_FONT_SIZE_MULTIPLIER,\n MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,\n platformFontWeightMedium,\n} from '../../utils/styles'\nimport Animated from 'react-native-reanimated'\nimport { TheirReplyConnector, MyReplyConnector } from './reply_connectors'\nimport { assertKeysAreNumbers } from '../../utils'\nimport { useNavigation } from '@react-navigation/native'\n\ninterface ShadowMessageProps extends MessageResource {\n conversation_id: number\n}\n\nexport function ShadowMessage({ conversation_id, ...message }: ShadowMessageProps) {\n const { text } = message\n const styles = useStyles(message)\n const { colors } = useTheme()\n const navigation = useNavigation()\n\n const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)\n const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =\n useAnimatedMessageBackgroundColor()\n const scalableNumberOfLines = useScalableNumberOfLines(2)\n\n const handleNavigateToReplies = () => {\n navigation.navigate('ConversationReply', {\n conversation_id,\n reply_root_id: message.id,\n // TODO: Add a way to pass the reply root author's name\n })\n }\n\n return (\n <Pressable\n android_ripple={{ color: colors.androidRippleNeutral }}\n onPress={handleNavigateToReplies}\n onPressIn={handleMessagePressIn}\n onPressOut={handleMessagePressOut}\n accessibilityHint=\"Navigate to reply screen for this message\"\n accessibilityRole=\"link\"\n >\n <Animated.View style={[styles.message, animatedBackgroundColor]}>\n {!message.mine && (\n <View>\n <View style={styles.avatarWrapper}>\n <Avatar\n size=\"xs\"\n sourceUri={message.author.avatar}\n style={styles.avatar}\n maxFontSizeMultiplier={1}\n />\n </View>\n <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />\n </View>\n )}\n <View style={styles.messageContent}>\n <View\n style={styles.messageBubble}\n onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}\n >\n <MessageAttachmentImagery attachments={message.attachments} />\n {text && (\n <Text\n variant=\"footnote\"\n style={styles.messageText}\n numberOfLines={scalableNumberOfLines}\n >\n {text}\n </Text>\n )}\n </View>\n <View style={styles.messageMeta}>\n <Text variant=\"footnote\" style={styles.replyCountText}>\n {message.replyCount} replies\n </Text>\n </View>\n </View>\n {message.mine && (\n <MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />\n )}\n </Animated.View>\n </Pressable>\n )\n}\n\nfunction MessageAttachmentImagery({\n attachments,\n}: {\n attachments: DenormalizedAttachmentResource[]\n}) {\n if (!attachments || attachments.length === 0) return null\n\n const attachment = attachments[0]\n\n if (attachment.type === 'giphy') {\n return <GiphyImage attachment={attachment} />\n }\n if (attachment.type === 'ExpandedLink') {\n return <ExpandedLinkImage attachment={attachment} />\n }\n if (attachment.type === 'MessageAttachment') {\n const contentType = attachment.attributes?.contentType\n const basicType = contentType?.split('/')[0]\n\n switch (basicType) {\n case 'image':\n return <MessageAttachmentImage attachment={attachment} />\n case 'video':\n return <MessageAttachmentIcon iconName=\"general.outlinedVideoFile\" />\n case 'audio':\n return <MessageAttachmentIcon iconName=\"general.outlinedMusicFile\" />\n case 'application':\n return <MessageAttachmentIcon iconName=\"general.outlinedGenericFile\" />\n default:\n return null\n }\n }\n\n return null\n}\n\nfunction GiphyImage({ attachment }: { attachment: DenormalizedGiphyAttachmentResource }) {\n const { title, giphy } = attachment\n const { url } = giphy.fixedWidth\n const { width, height } = assertKeysAreNumbers(giphy.fixedWidth)\n const styles = useStyles({ imageWidth: width, imageHeight: height })\n\n return (\n <Image\n source={{ uri: url }}\n wrapperStyle={styles.imageWrapper}\n style={styles.image}\n alt={title}\n loaderSize={16}\n />\n )\n}\n\nfunction ExpandedLinkImage({\n attachment,\n}: {\n attachment: DenormalizedExpandedLinkAttachmentResource\n}) {\n const { title = '', imageUrl, imageHeight, imageWidth } = attachment.attributes\n const styles = useStyles({ imageWidth, imageHeight })\n\n return (\n <Image\n source={{ uri: imageUrl }}\n wrapperStyle={styles.imageWrapper}\n style={styles.image}\n alt={title}\n loaderSize={16}\n />\n )\n}\n\nfunction MessageAttachmentImage({\n attachment,\n}: {\n attachment: DenormalizedMessageAttachmentResource\n}) {\n const { url, urlMedium, filename, metadata = {} } = attachment.attributes\n const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height })\n\n return (\n <Image\n source={{ uri: urlMedium || url }}\n style={styles.image}\n wrapperStyle={styles.imageWrapper}\n alt={filename}\n loaderSize={16}\n />\n )\n}\n\nfunction MessageAttachmentIcon({ iconName }: { iconName: IconProps['name'] }) {\n const styles = useStyles()\n return (\n <Icon\n name={iconName}\n style={styles.attachmentIcon}\n maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER}\n />\n )\n}\n\ninterface StylesProps {\n imageWidth?: number\n imageHeight?: number\n mine?: boolean\n}\n\nconst useStyles = ({ mine, imageWidth = 32, imageHeight = 32 }: StylesProps = {}) => {\n const { colors } = useTheme()\n const fontScale = useFontScale({ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER })\n const { width } = useWindowDimensions()\n const tabletWidth = width >= 744 // Smallest iPad Mini's width\n\n return StyleSheet.create({\n message: {\n gap: 8,\n flexDirection: mine ? 'row-reverse' : 'row',\n paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,\n },\n messageContent: {\n flex: 1,\n gap: 4,\n marginBottom: 12,\n },\n avatarWrapper: {\n width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,\n alignItems: 'center',\n },\n avatar: {\n marginBottom: 8,\n opacity: 0.5,\n },\n messageBubble: {\n flexDirection: 'row',\n alignSelf: mine ? 'flex-end' : 'flex-start',\n alignItems: 'center',\n gap: 8,\n borderColor: colors.borderColorDefaultBase,\n borderWidth: 1,\n borderRadius: 8,\n maxWidth: tabletWidth ? 360 : '80%',\n paddingVertical: 6,\n paddingHorizontal: 8,\n },\n messageText: {\n color: colors.textColorDefaultPlaceholder,\n flexShrink: 1,\n },\n messageMeta: {\n flexDirection: 'row',\n justifyContent: mine ? 'flex-end' : 'flex-start',\n },\n replyCountText: {\n color: colors.interaction,\n fontWeight: platformFontWeightMedium,\n },\n imageWrapper: {\n width: 32 * fontScale,\n aspectRatio: imageWidth / imageHeight,\n opacity: 0.5,\n },\n image: {\n borderRadius: 4,\n },\n attachmentIcon: {\n color: colors.iconColorDefaultDim,\n fontSize: 16,\n },\n })\n}\n"]}