@planningcenter/chat-react-native 3.16.2-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 +57 -23
  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 +14 -2
  32. package/build/screens/conversation_screen.d.ts.map +1 -1
  33. package/build/screens/conversation_screen.js +49 -2
  34. package/build/screens/conversation_screen.js.map +1 -1
  35. package/build/types/resources/message.d.ts +6 -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 +62 -31
  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 +77 -4
  68. package/src/types/resources/message.ts +6 -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,40 +20,47 @@ 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
23
-
24
- // TODO: Need to remove, just adding this to remove the lint warning about message being unused
25
- console.log('message', message)
26
+ if (messageBubbleHeight === 0) return null // Prevents UI shifting
26
27
 
27
28
  const connectorMap: Record<string, React.ReactNode | null> = {
28
29
  shortTailCurve: <ShortTailCurveConnector height={messageBubbleHeight / 2} />,
30
+ shortHeadCurve: <ShortHeadCurveConnector height={messageBubbleHeight / 2} />,
29
31
  verticalLine: <VerticalLineConnector />,
30
32
  default: null,
31
33
  }
32
34
 
33
- // TODO: Currently using fake message attributes. Will need to refactor once we have the message reply logic in place.
34
35
  const getConnectorKey = () => {
35
- // TODO: if (message.previousMessageIsTheirReplyMessage && message.nextMessageIsSomeoneElseReplyMessage) return 'shortTailCurve'
36
- // TODO: if (message.isRootMessage || (message.isTheirReplyMessage && message.nextMessageIsTheirReplyMessage) || (message.isTheirReplyMessage && message.nextMessageIsSomeoneElseReplyMessage && !message.nextMessageIsMyMessage)) return 'verticalLine'
36
+ if (threadPosition === 'first' && !renderAuthor) return 'shortHeadCurve'
37
+ if (threadPosition === 'last' && !renderAuthor) return 'shortTailCurve'
38
+ if (
39
+ (threadPosition === 'first' && renderAuthor) ||
40
+ threadPosition === 'center' ||
41
+ isReplyShadowMessage
42
+ )
43
+ return 'verticalLine'
37
44
  return 'default'
38
45
  }
39
46
 
40
47
  const ConnectorComponent = connectorMap[getConnectorKey()]
41
-
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
- // TODO: Refactor how we choose the correct connector once we have the logic in place
48
57
  export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnectorProps) {
49
58
  const styles = useStyles()
59
+ const { nextRendersAuthor, threadPosition, prevIsMyReply, nextIsMyReply, isReplyShadowMessage } =
60
+ message
50
61
 
51
62
  if (!REPLIES_FEATURE_ENABLED) return null
52
-
53
- // TODO: Need to remove, just adding this to remove the lint warning about message being unused
54
- console.log('message', message)
63
+ if (messageBubbleHeight === 0) return null // Prevents UI shifting
55
64
 
56
65
  const connectorMap: Record<string, React.ReactNode | null> = {
57
66
  longTailCurve: (
@@ -67,24 +76,25 @@ export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnecto
67
76
  />
68
77
  ),
69
78
  verticalLine: <VerticalLineConnector style={styles.myReplyConnectorPosition} />,
70
- swirl: <SwirlConnector style={styles.myReplyConnectorPosition} />,
79
+ swirl: <SwirlConnector style={styles.myReplyConnectorPosition} height={messageBubbleHeight} />,
71
80
  default: null,
72
81
  }
73
82
 
74
- // TODO: Currently using fake message attributes. Will need to refactor once we have the message reply logic in place.
75
83
  const getConnectorKey = () => {
76
- // TODO: if (message.currentMessageIsReply && (message.nextMessageIsNotReplyMessage || message.nextMessageIsDifferentReplyRootId)) return 'longTailCurve'
77
- // TODO: if (message.currentMessageIsReplyRoot && message.nextMessageIsMyReply) return 'longHeadCurve'
78
- // TODO: if (message.currentMessageIsReply && message.nextMessageIsReply) return 'verticalLine'
79
- // TODO: if (message.currentMessageIsReply && message.previousMessageSomeoneElseReply && (message.nextMessageIsSomeoneElseReply || message.nextMessageIsMyReply)) return 'swirl'
84
+ if (threadPosition === 'first' || isReplyShadowMessage) return 'longHeadCurve'
85
+ if (threadPosition === 'center' && prevIsMyReply && nextIsMyReply) return 'verticalLine'
86
+ if (threadPosition === 'center' && (!prevIsMyReply || !nextIsMyReply)) return 'swirl'
87
+ if (threadPosition === 'last') return 'longTailCurve'
88
+
80
89
  return 'default'
81
90
  }
82
91
 
83
92
  const ConnectorComponent = connectorMap[getConnectorKey()]
84
-
85
93
  if (!ConnectorComponent) return null
86
94
 
87
- return <View style={styles.myReplyConnectorContainer}>{ConnectorComponent}</View>
95
+ const spacerStyle = nextRendersAuthor ? styles.myReplyConnectorSpacer : null
96
+
97
+ return <View style={[styles.myReplyConnectorContainer, spacerStyle]}>{ConnectorComponent}</View>
88
98
  }
89
99
 
90
100
  function VerticalLineConnector({ style }: { style?: ViewStyle }) {
@@ -102,26 +112,33 @@ function LongTailCurveConnector({ height, style }: { height: number; style?: Vie
102
112
  return <View style={[styles.longTailCurveConnector, { height }, style]} />
103
113
  }
104
114
 
115
+ function ShortHeadCurveConnector({ height }: { height: number }) {
116
+ const styles = useStyles()
117
+ return <View style={[styles.shortHeadCurveConnector, { marginTop: height }]} />
118
+ }
119
+
105
120
  function ShortTailCurveConnector({ height }: { height: number }) {
106
121
  const styles = useStyles()
107
122
  return <View style={[styles.shortTailCurveConnector, { height }]} />
108
123
  }
109
124
 
110
- function SwirlConnector({ style }: { style?: ViewStyle }) {
125
+ function SwirlConnector({ style, height }: { style?: ViewStyle; height: number }) {
111
126
  const styles = useStyles()
112
127
  const { colors } = useTheme()
113
- const borderColor = colors.borderColorDefaultBase
114
128
 
115
129
  return (
116
130
  <View style={[styles.swirlConnectorContainer, style]}>
117
- <View style={styles.swirlConnectorVerticalLineHead} />
118
- <Svg width={27} height={34} fill="none">
119
- <Path
120
- stroke={borderColor}
121
- strokeWidth={CONNECTOR_BORDER_WIDTH}
122
- 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"
123
- />
124
- </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>
125
142
  <View style={styles.swirlConnectorVerticalLineTail} />
126
143
  </View>
127
144
  )
@@ -134,19 +151,26 @@ const useStyles = () => {
134
151
  return StyleSheet.create({
135
152
  theirReplyConnectorContainer: {
136
153
  width: '50%',
154
+ marginRight: OFFSET_CONNECTOR_CONTAINER,
137
155
  alignSelf: 'flex-end',
138
156
  flex: 1,
139
157
  },
158
+ theirReplyConnectorSpacer: {
159
+ paddingBottom: AVATAR_CONNECTOR_SPACING,
160
+ },
140
161
  myReplyConnectorContainer: {
141
162
  width: MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
142
163
  position: 'absolute',
143
- left: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
164
+ left: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL - OFFSET_CONNECTOR_CONTAINER,
144
165
  top: 0,
145
166
  bottom: 0,
146
167
  },
147
168
  myReplyConnectorPosition: {
148
169
  left: '50%',
149
170
  },
171
+ myReplyConnectorSpacer: {
172
+ bottom: AVATAR_CONNECTOR_SPACING,
173
+ },
150
174
  verticalLineConnector: {
151
175
  flex: 1,
152
176
  borderLeftWidth: CONNECTOR_BORDER_WIDTH,
@@ -167,6 +191,13 @@ const useStyles = () => {
167
191
  borderBottomWidth: CONNECTOR_BORDER_WIDTH,
168
192
  borderBottomLeftRadius: 16,
169
193
  },
194
+ shortHeadCurveConnector: {
195
+ borderColor: borderColor,
196
+ borderLeftWidth: CONNECTOR_BORDER_WIDTH,
197
+ borderTopWidth: CONNECTOR_BORDER_WIDTH,
198
+ borderTopLeftRadius: 16,
199
+ flex: 1,
200
+ },
170
201
  shortTailCurveConnector: {
171
202
  borderColor: borderColor,
172
203
  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
@@ -63,7 +65,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
63
65
  useConversationMessagesJoltEvents({ conversationId: conversation_id })
64
66
  useEnsureConversationsRouteExists()
65
67
  useMarkLatestMessageRead({ conversation, messages })
66
- const messagesWithSeparators = groupMessages(messages)
68
+ const messagesWithSeparators = groupMessages({ ms: messages, inReplyScreen: !!reply_root_id })
67
69
  const noMessages = messagesWithSeparators.length === 0
68
70
 
69
71
  const { repliesDisabled, memberAbility, badges, title } = conversation
@@ -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,14 +231,33 @@ const useDateSeparatorStyles = () => {
219
231
  })
220
232
  }
221
233
 
222
- export const groupMessages = (ms: MessageResource[]) => {
223
- let enrichedMessages: (MessageResource | DateSeparator)[] = []
234
+ type ReplyShadowMessage = {
235
+ type: 'ReplyShadowMessage'
236
+ id: string
237
+ messageId: string
238
+ isReplyShadowMessage: boolean
239
+ nextRendersAuthor: boolean
240
+ }
241
+
242
+ interface GroupMessagesProps {
243
+ ms: MessageResource[]
244
+ inReplyScreen?: boolean
245
+ }
246
+
247
+ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
248
+ let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
224
249
  let encounteredOneOfMyMessages = false
225
250
 
226
251
  ms.forEach((message, i) => {
227
252
  const prevMessage = ms[i + 1]
228
253
  const nextMessage = ms[i - 1]
229
254
  const date = moment(message.createdAt).format('YYYY-MM-DD')
255
+ const inThread = message.replyRootId !== null
256
+ const nextMessageInThread = nextMessage?.replyRootId !== null
257
+ const threadRoot = message.replyRootId === message.id
258
+ const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id
259
+ const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId
260
+ const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId
230
261
  const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id
231
262
  const nextMessageDifferentAuthor = message.author?.id !== nextMessage?.author?.id
232
263
  const prevMessageMoreThan5Minutes =
@@ -235,6 +266,14 @@ export const groupMessages = (ms: MessageResource[]) => {
235
266
  const nextMessageMoreThan5Minutes =
236
267
  nextMessage &&
237
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')
271
+ const nextMessageIsDateSeparator =
272
+ nextMessage && date !== moment(nextMessage.createdAt).format('YYYY-MM-DD')
273
+ const insertReplyShadowMessage =
274
+ message.replyRootId &&
275
+ !threadRoot &&
276
+ (prevMessageDifferentThread || prevMessageIsDateSeparator)
238
277
 
239
278
  if (message.mine && !encounteredOneOfMyMessages) {
240
279
  encounteredOneOfMyMessages = true
@@ -245,8 +284,42 @@ export const groupMessages = (ms: MessageResource[]) => {
245
284
  message.lastInGroup = !nextMessage || nextMessageDifferentAuthor || nextMessageMoreThan5Minutes
246
285
  message.renderAuthor =
247
286
  !message.mine && (!prevMessage || prevMessageDifferentAuthor || prevMessageMoreThan5Minutes)
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)
295
+
296
+ if (!inReplyScreen && inThread) {
297
+ message.prevIsMyReply = prevMessage?.mine
298
+ message.nextIsMyReply = nextMessage?.mine
299
+
300
+ const firstInThread = threadRoot
301
+ const lastInThread = nextMessageDifferentThread || nextMessageIsDateSeparator
302
+
303
+ if (firstInThread && lastInThread)
304
+ message.threadPosition = null // ensures we don't render a connector for root replies that aren't immediately followed up a reply
305
+ else if (firstInThread) message.threadPosition = 'first'
306
+ else if (lastInThread) message.threadPosition = 'last'
307
+ else if (!prevMessageDifferentThread && !nextMessageDifferentThread)
308
+ message.threadPosition = 'center'
309
+ }
248
310
 
249
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
+
250
323
  if (!prevMessage || date !== moment(prevMessage.createdAt).format('YYYY-MM-DD')) {
251
324
  enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
252
325
  }
@@ -23,6 +23,12 @@ export interface MessageResource {
23
23
  renderTime?: boolean
24
24
  myLatestInConversation?: boolean
25
25
  lastInGroup?: boolean
26
+ threadPosition?: 'first' | 'center' | 'last' | null
27
+ prevIsMyReply?: boolean
28
+ nextIsMyReply?: boolean
29
+ nextRendersAuthor?: boolean
30
+ isReplyShadowMessage?: boolean
31
+ nextIsReplyShadowMessage?: boolean
26
32
 
27
33
  // Properties for mutation state
28
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']