@planningcenter/chat-react-native 3.16.0-rc.3 → 3.16.0-rc.5

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 (80) hide show
  1. package/build/components/conversation/message.d.ts +2 -1
  2. package/build/components/conversation/message.d.ts.map +1 -1
  3. package/build/components/conversation/message.js +19 -3
  4. package/build/components/conversation/message.js.map +1 -1
  5. package/build/components/conversation/message_form.d.ts +2 -1
  6. package/build/components/conversation/message_form.d.ts.map +1 -1
  7. package/build/components/conversation/message_form.js +38 -16
  8. package/build/components/conversation/message_form.js.map +1 -1
  9. package/build/components/conversation/reply_connectors.js +1 -1
  10. package/build/components/conversation/reply_connectors.js.map +1 -1
  11. package/build/components/conversation/shadow_message.d.ts +2 -1
  12. package/build/components/conversation/shadow_message.d.ts.map +1 -1
  13. package/build/components/conversation/shadow_message.js +10 -5
  14. package/build/components/conversation/shadow_message.js.map +1 -1
  15. package/build/components/display/icon.d.ts +3 -1
  16. package/build/components/display/icon.d.ts.map +1 -1
  17. package/build/components/display/icon.js +2 -0
  18. package/build/components/display/icon.js.map +1 -1
  19. package/build/hooks/use_conversation_messages.d.ts +6 -3
  20. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  21. package/build/hooks/use_conversation_messages.js +32 -25
  22. package/build/hooks/use_conversation_messages.js.map +1 -1
  23. package/build/hooks/use_message_create_or_update.d.ts +2 -1
  24. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  25. package/build/hooks/use_message_create_or_update.js +6 -2
  26. package/build/hooks/use_message_create_or_update.js.map +1 -1
  27. package/build/hooks/use_suspense_api.d.ts +3 -1
  28. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  29. package/build/hooks/use_suspense_api.js +1 -0
  30. package/build/hooks/use_suspense_api.js.map +1 -1
  31. package/build/navigation/index.d.ts +6 -0
  32. package/build/navigation/index.d.ts.map +1 -1
  33. package/build/navigation/index.js +6 -0
  34. package/build/navigation/index.js.map +1 -1
  35. package/build/screens/conversation_screen.d.ts +2 -1
  36. package/build/screens/conversation_screen.d.ts.map +1 -1
  37. package/build/screens/conversation_screen.js +7 -5
  38. package/build/screens/conversation_screen.js.map +1 -1
  39. package/build/screens/message_actions_screen.d.ts +1 -0
  40. package/build/screens/message_actions_screen.d.ts.map +1 -1
  41. package/build/screens/message_actions_screen.js +16 -3
  42. package/build/screens/message_actions_screen.js.map +1 -1
  43. package/build/types/jolt_events/message_events.d.ts +2 -0
  44. package/build/types/jolt_events/message_events.d.ts.map +1 -1
  45. package/build/types/jolt_events/message_events.js.map +1 -1
  46. package/build/types/resources/message.d.ts +2 -0
  47. package/build/types/resources/message.d.ts.map +1 -1
  48. package/build/types/resources/message.js.map +1 -1
  49. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  50. package/build/utils/cache/optimistically_create_message.js +2 -0
  51. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  52. package/build/utils/index.d.ts +1 -0
  53. package/build/utils/index.d.ts.map +1 -1
  54. package/build/utils/index.js +1 -0
  55. package/build/utils/index.js.map +1 -1
  56. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  57. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +2 -0
  58. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  59. package/build/utils/replies_local_feature_flag.d.ts +2 -0
  60. package/build/utils/replies_local_feature_flag.d.ts.map +1 -0
  61. package/build/utils/replies_local_feature_flag.js +3 -0
  62. package/build/utils/replies_local_feature_flag.js.map +1 -0
  63. package/package.json +2 -2
  64. package/src/components/conversation/message.tsx +30 -2
  65. package/src/components/conversation/message_form.tsx +85 -15
  66. package/src/components/conversation/reply_connectors.tsx +1 -1
  67. package/src/components/conversation/shadow_message.tsx +15 -7
  68. package/src/components/display/icon.tsx +3 -0
  69. package/src/hooks/use_conversation_messages.ts +45 -25
  70. package/src/hooks/use_message_create_or_update.ts +7 -2
  71. package/src/hooks/use_suspense_api.ts +3 -0
  72. package/src/navigation/index.tsx +6 -0
  73. package/src/screens/conversation_screen.tsx +9 -4
  74. package/src/screens/message_actions_screen.tsx +27 -1
  75. package/src/types/jolt_events/message_events.ts +2 -0
  76. package/src/types/resources/message.ts +2 -0
  77. package/src/utils/cache/optimistically_create_message.ts +2 -0
  78. package/src/utils/index.ts +1 -0
  79. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +2 -0
  80. package/src/utils/replies_local_feature_flag.ts +2 -0
@@ -0,0 +1,3 @@
1
+ // TODO: Replace this whole file with the flipper flag
2
+ export const REPLIES_FEATURE_ENABLED = false;
3
+ //# sourceMappingURL=replies_local_feature_flag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replies_local_feature_flag.js","sourceRoot":"","sources":["../../src/utils/replies_local_feature_flag.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,CAAA","sourcesContent":["// TODO: Replace this whole file with the flipper flag\nexport const REPLIES_FEATURE_ENABLED = false\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.16.0-rc.3",
3
+ "version": "3.16.0-rc.5",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "react-native-url-polyfill": "^2.0.0",
56
56
  "typescript": "<5.6.0"
57
57
  },
58
- "gitHead": "41458eebf252d441367b1c30a6b35e5422486bf2"
58
+ "gitHead": "fbd2ce7be47ac372eb6bcd876c7db571681c7219"
59
59
  }
@@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
2
2
  import React, { useEffect } from 'react'
3
3
  import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
4
4
  import { MessageReaction } from '../../components/conversation/message_reaction'
5
- import { Avatar, Icon, Spinner, Text, TextInlineButton } from '../../components/display'
5
+ import { Avatar, Icon, Spinner, Text, TextButton, TextInlineButton } from '../../components/display'
6
6
  import {
7
7
  useAnimatedMessageBackgroundColor,
8
8
  useInteractionGhostBackgroundColor,
@@ -25,6 +25,7 @@ import { MessageReadReceipts } from './message_read_receipts'
25
25
  import { isNewMessage, useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
26
26
  import { Haptic } from '../../utils/native_adapters'
27
27
  import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
28
+ import { REPLIES_FEATURE_ENABLED } from '../../utils'
28
29
 
29
30
  /** Message
30
31
  * Component for display of a message within a conversation list
@@ -33,12 +34,14 @@ interface MessageProps extends MessageResource {
33
34
  canDeleteNonAuthoredMessages: boolean
34
35
  conversation_id: number
35
36
  latestReadMessageSortKey?: string
37
+ inReplyScreen?: boolean
36
38
  }
37
39
 
38
40
  export function Message({
39
41
  canDeleteNonAuthoredMessages,
40
42
  conversation_id,
41
43
  latestReadMessageSortKey,
44
+ inReplyScreen,
42
45
  ...message
43
46
  }: MessageProps) {
44
47
  const { text, reactionCounts, pending, error } = message
@@ -89,6 +92,7 @@ export function Message({
89
92
  message_id: message.id,
90
93
  conversation_id,
91
94
  canDeleteNonAuthoredMessages,
95
+ inReplyScreen,
92
96
  })
93
97
  }
94
98
  const handleReactionLongPress = (reaction: ReactionCountResource) => {
@@ -112,6 +116,14 @@ export function Message({
112
116
  })
113
117
  }
114
118
 
119
+ const handleNavigateToReplyPress = () => {
120
+ navigation.navigate('ConversationReply', {
121
+ conversation_id,
122
+ reply_root_id: message.id,
123
+ // TODO: Add a way to pass the reply root author's name
124
+ })
125
+ }
126
+
115
127
  const metaProps = {
116
128
  authorName: message.author.name,
117
129
  createdAt: message.createdAt,
@@ -120,6 +132,8 @@ export function Message({
120
132
  const renderAuthor = (!message.mine && message.renderAuthor) || false
121
133
  const messageBottomMargin = message.lastInGroup ? 12 : hasReactions || showMessageMeta ? 8 : 4
122
134
  const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
135
+ const showReplyCountButton =
136
+ !inReplyScreen && message.replyRootId === message.id && REPLIES_FEATURE_ENABLED
123
137
 
124
138
  return (
125
139
  <Pressable
@@ -127,7 +141,7 @@ export function Message({
127
141
  onPress={() => setShowMessageMetaToggle(!showMessageMetaToggle)}
128
142
  onPressIn={handleMessagePressIn}
129
143
  onPressOut={handleMessagePressOut}
130
- android_ripple={{ color: colors.androidRippleNeutral }}
144
+ android_ripple={{ color: colors.androidRippleNeutral, borderless: false, foreground: true }}
131
145
  accessibilityHint="Long press to view message actions like reacting and copying."
132
146
  >
133
147
  <Animated.View style={[styles.message, animatedBackgroundColor]}>
@@ -167,6 +181,17 @@ export function Message({
167
181
  </View>
168
182
  )}
169
183
  </View>
184
+ {showReplyCountButton && (
185
+ <TextButton
186
+ variant="footnote"
187
+ onPress={handleNavigateToReplyPress}
188
+ style={styles.messageReplyCount}
189
+ accessibilityHint="Navigates to reply screen for this message"
190
+ accessibilityRole="link"
191
+ >
192
+ {message.replyCount} replies
193
+ </TextButton>
194
+ )}
170
195
  {hasReactions && (
171
196
  <View style={styles.messageReactions}>
172
197
  {reactionCounts.map(reaction => (
@@ -277,6 +302,9 @@ const useMessageStyles = ({ mine }: MessageResource) => {
277
302
  gap: 4,
278
303
  justifyContent: mine ? 'flex-end' : 'flex-start',
279
304
  },
305
+ messageReplyCount: {
306
+ alignSelf: mine ? 'flex-end' : 'flex-start',
307
+ },
280
308
  messageMeta: {
281
309
  flexDirection: 'row',
282
310
  alignItems: 'center',
@@ -15,11 +15,12 @@ import {
15
15
  ViewProps,
16
16
  Keyboard,
17
17
  } from 'react-native'
18
- import { Icon, IconButton, Text, TextButton } from '../../components'
18
+ import { Heading, Icon, IconButton, IconString, Text, TextButton } from '../../components'
19
19
  import {
20
20
  useCreateAndroidRippleColor,
21
21
  useTheme,
22
22
  useInteractionGhostBackgroundColor,
23
+ useScalableNumberOfLines,
23
24
  } from '../../hooks'
24
25
  import { ConversationResource, MessageResource } from '../../types'
25
26
 
@@ -27,7 +28,12 @@ import { ConversationScreenProps } from '../../screens/conversation_screen'
27
28
 
28
29
  import { ChatContext } from '../../contexts/chat_context'
29
30
  import { Haptic, ImagePicker, ImagePickerResult } from '../../utils/native_adapters'
30
- import { platformFontWeightMedium, platformPressedOpacityStyle } from '../../utils'
31
+ import {
32
+ MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
33
+ REPLIES_FEATURE_ENABLED,
34
+ platformFontWeightMedium,
35
+ platformPressedOpacityStyle,
36
+ } from '../../utils'
31
37
  import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
32
38
  import { useMessageDraft } from '../../hooks/use_message_draft'
33
39
  import {
@@ -53,6 +59,7 @@ export const MessageForm = {
53
59
  interface MessagesFormRootProps extends ViewProps {
54
60
  conversation: ConversationResource
55
61
  currentlyEditingMessage?: MessageResource | null
62
+ replyRootId?: string
56
63
  }
57
64
 
58
65
  const MessageFormContext = React.createContext<{
@@ -66,6 +73,7 @@ const MessageFormContext = React.createContext<{
66
73
  attachmentUploader?: ReturnType<typeof useAttachmentUploader>
67
74
  currentlyEditingMessage?: MessageResource | null
68
75
  reset: () => void
76
+ replyRootId?: string
69
77
  }>({
70
78
  text: '',
71
79
  setText: (_text: string) => {},
@@ -76,6 +84,7 @@ const MessageFormContext = React.createContext<{
76
84
  setUsingGiphy: (_usingGiphy: boolean) => {},
77
85
  currentlyEditingMessage: null,
78
86
  reset: () => {},
87
+ replyRootId: undefined,
79
88
  })
80
89
 
81
90
  const GIPHY_MAX_LENGTH = 50
@@ -85,6 +94,7 @@ function MessageFormRoot({
85
94
  conversation,
86
95
  currentlyEditingMessage,
87
96
  children,
97
+ replyRootId,
88
98
  }: MessagesFormRootProps) {
89
99
  const { giphyApiKey } = useContext(ChatContext)
90
100
  const canGiphy = !!giphyApiKey && !currentlyEditingMessage
@@ -105,6 +115,7 @@ function MessageFormRoot({
105
115
  } = useMessageCreateOrUpdate({
106
116
  conversationId: conversation.id,
107
117
  message: currentlyEditingMessage || undefined,
118
+ replyRootId,
108
119
  })
109
120
  const attachmentUploader = useAttachmentUploader({
110
121
  conversationId: conversation.id,
@@ -236,10 +247,12 @@ function MessageFormRoot({
236
247
  attachmentUploader,
237
248
  currentlyEditingMessage,
238
249
  reset,
250
+ replyRootId,
239
251
  }}
240
252
  >
241
253
  <View style={styles.container}>
242
254
  <EditingIndicator />
255
+ <ReplyIndicator />
243
256
  <View style={styles.textInputContainer}>{children}</View>
244
257
  </View>
245
258
  </MessageFormContext.Provider>
@@ -529,25 +542,76 @@ function MessageFormCommands() {
529
542
 
530
543
  function EditingIndicator() {
531
544
  const { currentlyEditingMessage, reset } = React.useContext(MessageFormContext)
532
- const styles = useMessageFormStyles()
533
545
 
534
546
  if (!currentlyEditingMessage) {
535
547
  return null
536
548
  }
537
549
 
538
550
  return (
539
- <View style={styles.editingIndicator}>
540
- <View style={styles.editingIndicatorTitleContainer}>
541
- <Icon name="accounts.editor" size={16} />
542
- <Text variant="tertiary" style={styles.editingIndicatorTitle}>
543
- Editing message
544
- </Text>
551
+ <FormIndicatorRow
552
+ title="Editing message"
553
+ buttonText="Cancel"
554
+ accessibilityHint="Exit message editing mode without saving"
555
+ iconName="accounts.editor"
556
+ onPress={() => reset()}
557
+ />
558
+ )
559
+ }
560
+
561
+ function ReplyIndicator() {
562
+ const { replyRootId } = React.useContext(MessageFormContext)
563
+ const navigation = useNavigation()
564
+
565
+ if (!REPLIES_FEATURE_ENABLED || !replyRootId) return null
566
+
567
+ return (
568
+ <FormIndicatorRow
569
+ title="Reply" // TODO: Get root reply author
570
+ buttonText="Exit replies"
571
+ accessibilityHint="Return to the main conversation"
572
+ iconName="registrations.undo"
573
+ onPress={() => navigation.goBack()}
574
+ />
575
+ )
576
+ }
577
+
578
+ interface FormIndicatorRowProps {
579
+ title: string
580
+ buttonText: string
581
+ accessibilityHint: string
582
+ onPress: () => void
583
+ iconName: IconString
584
+ }
585
+
586
+ function FormIndicatorRow({
587
+ title,
588
+ buttonText,
589
+ accessibilityHint,
590
+ iconName,
591
+ onPress,
592
+ }: FormIndicatorRowProps) {
593
+ const styles = useMessageFormStyles()
594
+ const scalableNumberOfLines = useScalableNumberOfLines(1)
595
+
596
+ return (
597
+ <View style={styles.formIndicatorRow}>
598
+ <View style={styles.formIndicatorRowTitleContainer}>
599
+ <Icon name={iconName} size={16} style={styles.formIndicatorRowIcon} />
600
+ <Heading
601
+ variant="h4"
602
+ style={styles.formIndicatorRowTitle}
603
+ numberOfLines={scalableNumberOfLines}
604
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
605
+ >
606
+ {title}
607
+ </Heading>
545
608
  </View>
546
609
  <TextButton
547
- onPress={() => reset()}
548
- accessibilityHint="Exit message editing mode without saving"
610
+ onPress={onPress}
611
+ accessibilityHint={accessibilityHint}
612
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
549
613
  >
550
- Cancel
614
+ {buttonText}
551
615
  </TextButton>
552
616
  </View>
553
617
  )
@@ -681,20 +745,26 @@ const useMessageFormStyles = () => {
681
745
  paddingHorizontal: 16,
682
746
  paddingVertical: 4,
683
747
  },
684
- editingIndicator: {
748
+ formIndicatorRow: {
685
749
  flexDirection: 'row',
686
750
  alignItems: 'center',
687
751
  justifyContent: 'space-between',
688
752
  gap: 8,
689
753
  paddingBottom: 12,
690
754
  },
691
- editingIndicatorTitleContainer: {
755
+ formIndicatorRowTitleContainer: {
692
756
  flexDirection: 'row',
693
757
  alignItems: 'center',
694
758
  gap: 8,
759
+ flex: 1,
760
+ },
761
+ formIndicatorRowIcon: {
762
+ color: theme.colors.iconColorDefaultSecondary,
695
763
  },
696
- editingIndicatorTitle: {
764
+ formIndicatorRowTitle: {
697
765
  fontWeight: platformFontWeightMedium,
766
+ textTransform: 'none',
767
+ flexShrink: 1,
698
768
  },
699
769
  })
700
770
  }
@@ -6,7 +6,7 @@ import {
6
6
  CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
7
7
  MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
8
8
  } from '../../utils/styles'
9
- import { REPLIES_FEATURE_ENABLED } from '../../screens/conversation_screen'
9
+ import { REPLIES_FEATURE_ENABLED } from '../../utils'
10
10
 
11
11
  const MY_REPLY_CONNECTOR_WIDTH = 38
12
12
  const CONNECTOR_BORDER_WIDTH = 4
@@ -23,22 +23,29 @@ import {
23
23
  import Animated from 'react-native-reanimated'
24
24
  import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
25
25
  import { assertKeysAreNumbers } from '../../utils'
26
+ import { useNavigation } from '@react-navigation/native'
26
27
 
27
- interface ShadowMessageProps extends MessageResource {}
28
+ interface ShadowMessageProps extends MessageResource {
29
+ conversation_id: number
30
+ }
28
31
 
29
- export function ShadowMessage({ ...message }: ShadowMessageProps) {
32
+ export function ShadowMessage({ conversation_id, ...message }: ShadowMessageProps) {
30
33
  const { text } = message
31
34
  const styles = useStyles(message)
32
35
  const { colors } = useTheme()
36
+ const navigation = useNavigation()
37
+
33
38
  const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
34
39
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
35
40
  useAnimatedMessageBackgroundColor()
36
41
  const scalableNumberOfLines = useScalableNumberOfLines(2)
37
42
 
38
- const replyCount = 0 // TODO: Get reply count from message object
39
-
40
43
  const handleNavigateToReplies = () => {
41
- console.log('Navigate to replies') // TODO: Implement navigate to a reply screen
44
+ navigation.navigate('ConversationReply', {
45
+ conversation_id,
46
+ reply_root_id: message.id,
47
+ // TODO: Add a way to pass the reply root author's name
48
+ })
42
49
  }
43
50
 
44
51
  return (
@@ -47,7 +54,8 @@ export function ShadowMessage({ ...message }: ShadowMessageProps) {
47
54
  onPress={handleNavigateToReplies}
48
55
  onPressIn={handleMessagePressIn}
49
56
  onPressOut={handleMessagePressOut}
50
- accessibilityHint="Navigate to all replies for this message."
57
+ accessibilityHint="Navigate to reply screen for this message"
58
+ accessibilityRole="link"
51
59
  >
52
60
  <Animated.View style={[styles.message, animatedBackgroundColor]}>
53
61
  {!message.mine && (
@@ -81,7 +89,7 @@ export function ShadowMessage({ ...message }: ShadowMessageProps) {
81
89
  </View>
82
90
  <View style={styles.messageMeta}>
83
91
  <Text variant="footnote" style={styles.replyCountText}>
84
- {replyCount} replies
92
+ {message.replyCount} replies
85
93
  </Text>
86
94
  </View>
87
95
  </View>
@@ -17,6 +17,7 @@ import * as logomark from '@planningcenter/icons/paths/logomark'
17
17
  import * as people from '@planningcenter/icons/paths/people'
18
18
  import * as services from '@planningcenter/icons/paths/services'
19
19
  import * as publishing from '@planningcenter/icons/paths/publishing'
20
+ import * as registrations from '@planningcenter/icons/paths/registrations'
20
21
 
21
22
  // =================================
22
23
  // ====== Constants ================
@@ -37,6 +38,7 @@ const ICONS = {
37
38
  people,
38
39
  services,
39
40
  publishing,
41
+ registrations,
40
42
  } as const
41
43
 
42
44
  export type IconStyle = ViewStyle & {
@@ -61,6 +63,7 @@ export type IconString =
61
63
  | `people.${IconName<'people'>}`
62
64
  | `services.${IconName<'services'>}`
63
65
  | `publishing.${IconName<'publishing'>}`
66
+ | `registrations.${IconName<'registrations'>}`
64
67
 
65
68
  // =================================
66
69
  // ====== Component ================
@@ -7,14 +7,14 @@ import {
7
7
  } from './use_suspense_api'
8
8
 
9
9
  export const useConversationMessages = (
10
- { conversation_id }: { conversation_id: number },
10
+ { conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string },
11
11
  opts?: SuspensePaginatorOptions
12
12
  ) => {
13
13
  const { data, refetch, isRefetching, fetchNextPage } = useSuspensePaginator<MessageResource>(
14
- getMessagesRequestArgs({ conversation_id }),
14
+ getMessagesRequestArgs({ conversation_id, reply_root_id }),
15
15
  opts
16
16
  )
17
- const queryKey = getMessagesQueryKey({ conversation_id })
17
+ const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
18
18
  const messages = useMemo(
19
19
  () =>
20
20
  data
@@ -28,29 +28,49 @@ export const useConversationMessages = (
28
28
  return { messages, refetch, isRefetching, fetchNextPage, queryKey }
29
29
  }
30
30
 
31
- export const getMessagesRequestArgs = ({ conversation_id }: { conversation_id: number }) => ({
32
- url: `/me/conversations/${conversation_id}/messages`,
33
- data: {
34
- perPage: 25,
35
- fields: {
36
- Message: [
37
- 'text',
38
- 'text_edited_at',
39
- 'mine',
40
- 'attachments',
41
- 'created_at',
42
- 'deleted_at',
43
- 'author',
44
- 'reaction_counts',
45
- ],
46
- Person: ['name', 'avatar'],
47
- ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],
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'],
48
63
  },
49
- include: ['author', 'reaction_counts'],
50
- },
51
- })
64
+ }
65
+ }
52
66
 
53
- export const getMessagesQueryKey = ({ conversation_id }: { conversation_id: number }) => {
54
- const requestArgs = getMessagesRequestArgs({ conversation_id })
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 })
55
75
  return getRequestQueryKey(requestArgs)
56
76
  }
@@ -18,9 +18,10 @@ import { startMessageCreationTracking } from '../utils/performance_tracking'
18
18
  interface Props {
19
19
  conversationId: number
20
20
  message?: MessageResource
21
+ replyRootId?: string
21
22
  }
22
23
 
23
- export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
24
+ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {
24
25
  const messageId = message?.id || null
25
26
  const isEditing = !isNewMessage(message)
26
27
  const apiClient = useApiClient()
@@ -38,7 +39,11 @@ export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
38
39
  const fieldsWithValueJoined = Object.fromEntries(
39
40
  Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
40
41
  )
41
- let attributes: any = { text, ...(attachments ? { attachments } : {}) }
42
+ let attributes: any = {
43
+ text,
44
+ ...(attachments ? { attachments } : {}),
45
+ ...(replyRootId ? { reply_root: { id: replyRootId } } : {}),
46
+ }
42
47
  if (!isEditing) {
43
48
  const idempotentKey = insecureUUID()
44
49
  attributes.idempotent_key = idempotentKey
@@ -14,6 +14,7 @@ import { ResponseError } from '../utils/response_error'
14
14
 
15
15
  interface SuspenseGetOptions extends GetRequest {
16
16
  app?: App
17
+ reply_root_id?: string
17
18
  }
18
19
 
19
20
  export type SuspenseGetQueryOptions<T extends ResourceObject | ResourceObject[]> = Omit<
@@ -114,10 +115,12 @@ export type RequestQueryKey = [
114
115
  SuspenseGetOptions['data'],
115
116
  SuspenseGetOptions['headers'],
116
117
  SuspenseGetOptions['app'],
118
+ SuspenseGetOptions['reply_root_id'],
117
119
  ]
118
120
  export const getRequestQueryKey = (args: SuspenseGetOptions): RequestQueryKey => [
119
121
  args.url,
120
122
  args.data,
121
123
  args.headers,
122
124
  args.app || 'chat',
125
+ args.reply_root_id,
123
126
  ]
@@ -184,6 +184,12 @@ export const ChatStack = createNativeStackNavigator({
184
184
  ),
185
185
  }),
186
186
  },
187
+ ConversationReply: {
188
+ screen: ConversationScreen,
189
+ options: {
190
+ title: 'Reply', // TODO: Get root reply author
191
+ },
192
+ },
187
193
  TeamConversation: {
188
194
  screen: TeamConversationScreen,
189
195
  options: {
@@ -35,10 +35,10 @@ 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
37
 
38
- export const REPLIES_FEATURE_ENABLED = false
39
-
40
38
  export type ConversationRouteProps = {
41
39
  conversation_id: number
40
+ reply_root_id?: string
41
+ replyRootAuthor?: string
42
42
  chat_group_graph_id?: string
43
43
  clear_input?: boolean
44
44
  editing_message_id?: number | null
@@ -53,10 +53,11 @@ export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
53
53
  export function ConversationScreen({ route }: ConversationScreenProps) {
54
54
  const styles = useStyles()
55
55
  const navigation = useNavigation()
56
- const { conversation_id, editing_message_id } = route.params
56
+ const { conversation_id, editing_message_id, reply_root_id } = route.params
57
57
  const { data: conversation } = useConversation(route.params)
58
58
  const { messages, refetch, isRefetching, fetchNextPage } = useConversationMessages({
59
59
  conversation_id,
60
+ reply_root_id,
60
61
  })
61
62
  useConversationJoltEvents({ conversationId: conversation_id })
62
63
  useConversationMessagesJoltEvents({ conversationId: conversation_id })
@@ -86,12 +87,14 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
86
87
  }, [])
87
88
 
88
89
  useEffect(() => {
90
+ if (reply_root_id) return
91
+
89
92
  navigation.setParams({
90
93
  title: title,
91
94
  badge: badges?.[0],
92
95
  deleted: conversation?.deleted,
93
96
  })
94
- }, [navigation, title, badges, conversation?.deleted])
97
+ }, [navigation, title, badges, conversation?.deleted, reply_root_id])
95
98
 
96
99
  if (!conversation || conversation.deleted) {
97
100
  return (
@@ -139,6 +142,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
139
142
  canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
140
143
  conversation_id={conversation_id}
141
144
  latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
145
+ inReplyScreen={!!reply_root_id}
142
146
  />
143
147
  )
144
148
  }}
@@ -152,6 +156,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
152
156
  {canReply ? (
153
157
  <MessageForm.Root
154
158
  conversation={conversation}
159
+ replyRootId={reply_root_id}
155
160
  currentlyEditingMessage={currentlyEditingMessage}
156
161
  // We use a separate key so that it remounts component when switching between new
157
162
  // and edit message. This simplifies internal state handling.