@planningcenter/chat-react-native 3.18.0-rc.1 → 3.18.0-rc.11

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 (137) 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 +53 -26
  4. package/build/components/conversation/message.js.map +1 -1
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +7 -6
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversation/messages_disabled_banners.d.ts +3 -0
  9. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -0
  10. package/build/components/conversation/messages_disabled_banners.js +53 -0
  11. package/build/components/conversation/messages_disabled_banners.js.map +1 -0
  12. package/build/components/conversation/reply_connectors.d.ts.map +1 -1
  13. package/build/components/conversation/reply_connectors.js +0 -5
  14. package/build/components/conversation/reply_connectors.js.map +1 -1
  15. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  16. package/build/components/conversation/reply_shadow_message.js +8 -3
  17. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  18. package/build/components/conversation/typing_indicator.d.ts +1 -5
  19. package/build/components/conversation/typing_indicator.d.ts.map +1 -1
  20. package/build/components/conversation/typing_indicator.js +2 -2
  21. package/build/components/conversation/typing_indicator.js.map +1 -1
  22. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  23. package/build/components/conversations/conversation_actions.js +1 -2
  24. package/build/components/conversations/conversation_actions.js.map +1 -1
  25. package/build/components/conversations/conversations.d.ts.map +1 -1
  26. package/build/components/conversations/conversations.js +2 -3
  27. package/build/components/conversations/conversations.js.map +1 -1
  28. package/build/contexts/conversation_context.d.ts +13 -0
  29. package/build/contexts/conversation_context.d.ts.map +1 -0
  30. package/build/contexts/conversation_context.js +14 -0
  31. package/build/contexts/conversation_context.js.map +1 -0
  32. package/build/hooks/use_broadcast_typing_status.d.ts +1 -1
  33. package/build/hooks/use_broadcast_typing_status.d.ts.map +1 -1
  34. package/build/hooks/use_broadcast_typing_status.js +7 -3
  35. package/build/hooks/use_broadcast_typing_status.js.map +1 -1
  36. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  37. package/build/hooks/use_conversation_messages.js +2 -1
  38. package/build/hooks/use_conversation_messages.js.map +1 -1
  39. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  40. package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
  41. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  42. package/build/hooks/use_features.d.ts +9 -0
  43. package/build/hooks/use_features.d.ts.map +1 -0
  44. package/build/hooks/use_features.js +35 -0
  45. package/build/hooks/use_features.js.map +1 -0
  46. package/build/hooks/use_message_create_or_update.d.ts +0 -2
  47. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  48. package/build/hooks/use_message_create_or_update.js +10 -8
  49. package/build/hooks/use_message_create_or_update.js.map +1 -1
  50. package/build/hooks/use_typing_indicators.d.ts +1 -1
  51. package/build/hooks/use_typing_indicators.d.ts.map +1 -1
  52. package/build/hooks/use_typing_indicators.js +16 -3
  53. package/build/hooks/use_typing_indicators.js.map +1 -1
  54. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  55. package/build/screens/conversation_details_screen.js +9 -6
  56. package/build/screens/conversation_details_screen.js.map +1 -1
  57. package/build/screens/conversation_new/components/form_list.d.ts +2 -2
  58. package/build/screens/conversation_new/components/form_list.d.ts.map +1 -1
  59. package/build/screens/conversation_new/components/form_list.js +2 -3
  60. package/build/screens/conversation_new/components/form_list.js.map +1 -1
  61. package/build/screens/conversation_screen.d.ts +2 -1
  62. package/build/screens/conversation_screen.d.ts.map +1 -1
  63. package/build/screens/conversation_screen.js +41 -18
  64. package/build/screens/conversation_screen.js.map +1 -1
  65. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.d.ts.map +1 -1
  66. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js +2 -3
  67. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js.map +1 -1
  68. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
  69. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +2 -3
  70. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
  71. package/build/screens/message_actions_screen.js +4 -2
  72. package/build/screens/message_actions_screen.js.map +1 -1
  73. package/build/types/jolt_events/reaction_events.d.ts +1 -0
  74. package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
  75. package/build/types/jolt_events/reaction_events.js.map +1 -1
  76. package/build/types/jolt_events/typing_events.d.ts +1 -0
  77. package/build/types/jolt_events/typing_events.d.ts.map +1 -1
  78. package/build/types/jolt_events/typing_events.js.map +1 -1
  79. package/build/types/resources/feature_resource.d.ts +7 -0
  80. package/build/types/resources/feature_resource.d.ts.map +1 -0
  81. package/build/types/resources/feature_resource.js +2 -0
  82. package/build/types/resources/feature_resource.js.map +1 -0
  83. package/build/utils/cache/messages_cache.d.ts +9 -0
  84. package/build/utils/cache/messages_cache.d.ts.map +1 -0
  85. package/build/utils/cache/messages_cache.js +89 -0
  86. package/build/utils/cache/messages_cache.js.map +1 -0
  87. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  88. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  89. package/build/utils/cache/optimistically_create_message.js +6 -3
  90. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  91. package/build/utils/index.d.ts +0 -1
  92. package/build/utils/index.d.ts.map +1 -1
  93. package/build/utils/index.js +0 -1
  94. package/build/utils/index.js.map +1 -1
  95. package/build/utils/request/get_features.d.ts +11 -0
  96. package/build/utils/request/get_features.d.ts.map +1 -0
  97. package/build/utils/request/get_features.js +18 -0
  98. package/build/utils/request/get_features.js.map +1 -0
  99. package/package.json +2 -3
  100. package/src/components/conversation/message.tsx +80 -29
  101. package/src/components/conversation/message_form.tsx +6 -11
  102. package/src/components/conversation/messages_disabled_banners.tsx +69 -0
  103. package/src/components/conversation/reply_connectors.tsx +0 -3
  104. package/src/components/conversation/reply_shadow_message.tsx +9 -2
  105. package/src/components/conversation/typing_indicator.tsx +2 -6
  106. package/src/components/conversations/conversation_actions.tsx +1 -1
  107. package/src/components/conversations/conversations.tsx +7 -9
  108. package/src/contexts/conversation_context.tsx +34 -0
  109. package/src/hooks/use_broadcast_typing_status.ts +7 -3
  110. package/src/hooks/use_conversation_messages.ts +3 -1
  111. package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
  112. package/src/hooks/use_features.ts +47 -0
  113. package/src/hooks/use_message_create_or_update.ts +10 -9
  114. package/src/hooks/use_typing_indicators.ts +15 -3
  115. package/src/screens/conversation_details_screen.tsx +9 -6
  116. package/src/screens/conversation_new/components/form_list.tsx +3 -5
  117. package/src/screens/conversation_screen.tsx +58 -20
  118. package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx +2 -4
  119. package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +2 -4
  120. package/src/screens/message_actions_screen.tsx +4 -2
  121. package/src/types/jolt_events/reaction_events.ts +1 -0
  122. package/src/types/jolt_events/typing_events.ts +1 -0
  123. package/src/types/resources/feature_resource.ts +6 -0
  124. package/src/utils/cache/messages_cache.ts +113 -0
  125. package/src/utils/cache/optimistically_create_message.ts +7 -2
  126. package/src/utils/index.ts +0 -1
  127. package/src/utils/request/get_features.ts +20 -0
  128. package/build/components/conversation/disabled_replies_banners.d.ts +0 -3
  129. package/build/components/conversation/disabled_replies_banners.d.ts.map +0 -1
  130. package/build/components/conversation/disabled_replies_banners.js +0 -41
  131. package/build/components/conversation/disabled_replies_banners.js.map +0 -1
  132. package/build/utils/replies_local_feature_flag.d.ts +0 -2
  133. package/build/utils/replies_local_feature_flag.d.ts.map +0 -1
  134. package/build/utils/replies_local_feature_flag.js +0 -3
  135. package/build/utils/replies_local_feature_flag.js.map +0 -1
  136. package/src/components/conversation/disabled_replies_banners.tsx +0 -58
  137. package/src/utils/replies_local_feature_flag.ts +0 -2
@@ -1,6 +1,12 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React, { useEffect } from 'react'
3
- import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
3
+ import {
4
+ AccessibilityActionEvent,
5
+ Pressable,
6
+ StyleSheet,
7
+ useWindowDimensions,
8
+ View,
9
+ } from 'react-native'
4
10
  import { MessageReaction } from '../../components/conversation/message_reaction'
5
11
  import { Avatar, Icon, Spinner, Text, TextButton, TextInlineButton } from '../../components/display'
6
12
  import {
@@ -22,11 +28,13 @@ import {
22
28
  import Animated from 'react-native-reanimated'
23
29
  import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
24
30
  import { MessageReadReceipts } from './message_read_receipts'
25
- import { isNewMessage, useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
31
+ import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
26
32
  import { Haptic } from '../../utils/native_adapters'
27
33
  import { TheirReplyConnector, MyReplyConnector, AVATAR_CONNECTOR_SPACING } from './reply_connectors'
28
- import { pluralize, REPLIES_FEATURE_ENABLED } from '../../utils'
34
+ import { pluralize } from '../../utils'
29
35
  import { useConversationMessage } from '../../hooks/use_conversation_message'
36
+ import { some } from 'lodash'
37
+ import { isNewMessage } from '../../utils/cache/messages_cache'
30
38
 
31
39
  /** Message
32
40
  * Component for display of a message within a conversation list
@@ -36,6 +44,7 @@ interface MessageProps extends MessageResource {
36
44
  conversation_id: number
37
45
  latestReadMessageSortKey?: string
38
46
  inReplyScreen?: boolean
47
+ repliesEnabled?: boolean
39
48
  }
40
49
 
41
50
  export function Message({
@@ -43,9 +52,10 @@ export function Message({
43
52
  conversation_id,
44
53
  latestReadMessageSortKey,
45
54
  inReplyScreen,
55
+ repliesEnabled,
46
56
  ...message
47
57
  }: MessageProps) {
48
- const { text, reactionCounts, pending, error } = message
58
+ const { text, reactionCounts, pending, error, attachments, author } = message
49
59
  const styles = useMessageStyles(message)
50
60
  const navigation = useNavigation()
51
61
  const { colors } = useTheme()
@@ -67,22 +77,21 @@ export function Message({
67
77
  })
68
78
 
69
79
  const metaProps = {
70
- authorName: message.author.name,
80
+ authorName: author.name,
71
81
  createdAt: message.createdAt,
72
82
  }
73
83
 
74
84
  const renderAuthor = (!message.mine && message.renderAuthor) || false
75
85
  const showReplyCountButton =
76
- !inReplyScreen && message.replyRootId === message.id && REPLIES_FEATURE_ENABLED
86
+ !inReplyScreen && message.replyRootId === message.id && repliesEnabled
77
87
  const isReplyRootMessage = message.replyRootId === message.id
78
88
  const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
89
+ const replyToReplyRootMessage = repliesEnabled && !isReplyRootMessage && !!message.replyRootId
79
90
 
80
- const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
81
91
  const replyCountText = pluralize(message.replyCount, 'reply')
82
92
  const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
83
- const replyRootAuthorName = message.replyRootId
84
- ? replyRootMessage?.author.name
85
- : message.author.name
93
+ const replyRootAuthorName = message.replyRootId ? replyRootMessage?.author.name : author.name
94
+ const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
86
95
 
87
96
  const messageBottomMargin =
88
97
  message.lastInGroup || message.nextIsReplyShadowMessage
@@ -91,6 +100,11 @@ export function Message({
91
100
  ? 8
92
101
  : 4
93
102
 
103
+ const messageIsReplyLabel = replyToReplyRootMessage ? 'Reply' : ''
104
+ const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : ''
105
+ const replyCountLabel = isReplyRootMessage ? replyCountText : ''
106
+ const accessibilityLabel = `${author?.name || ''} ${messageIsReplyLabel} ${attachmentLabel} ${messageText || ''} ${timestamp} ${replyCountLabel}`
107
+
94
108
  useEffect(() => {
95
109
  if (pending) {
96
110
  const timer = setTimeout(() => {
@@ -115,6 +129,10 @@ export function Message({
115
129
  }
116
130
  }
117
131
 
132
+ const handleMessagePress = () => {
133
+ setShowMessageMetaToggle(!showMessageMetaToggle)
134
+ }
135
+
118
136
  const handleMessageLongPress = () => {
119
137
  if (!isPersisted) return
120
138
 
@@ -127,6 +145,7 @@ export function Message({
127
145
  reply_root_author_name: replyRootAuthorName,
128
146
  })
129
147
  }
148
+
130
149
  const handleReactionLongPress = (reaction: ReactionCountResource) => {
131
150
  Haptic.impactLight()
132
151
  navigation.navigate('Reactions', {
@@ -152,18 +171,36 @@ export function Message({
152
171
  navigation.navigate('ConversationReply', {
153
172
  conversation_id,
154
173
  reply_root_id: message.id,
155
- reply_root_author_name: message.author.name,
174
+ reply_root_author_name: author.name,
156
175
  })
157
176
  }
158
177
 
178
+ const accessibilityActions = [
179
+ {
180
+ name: 'navigateToReplies',
181
+ label: 'Navigate to replies',
182
+ },
183
+ ]
184
+
185
+ const handleAccessibilityAction = (event: AccessibilityActionEvent) => {
186
+ if (event.nativeEvent.actionName === 'navigateToReplies') {
187
+ handleNavigateToReplyPress()
188
+ }
189
+ }
190
+
159
191
  return (
160
192
  <Pressable
161
193
  onLongPress={handleMessageLongPress}
162
- onPress={() => setShowMessageMetaToggle(!showMessageMetaToggle)}
194
+ onPress={handleMessagePress}
163
195
  onPressIn={handleMessagePressIn}
164
196
  onPressOut={handleMessagePressOut}
165
197
  android_ripple={{ color: colors.androidRippleNeutral, borderless: false, foreground: true }}
198
+ accessibilityRole="button"
199
+ accessibilityLabel={accessibilityLabel}
166
200
  accessibilityHint="Long press to view message actions like reacting and copying."
201
+ accessibilityActions={isReplyRootMessage ? accessibilityActions : undefined}
202
+ onAccessibilityAction={isReplyRootMessage ? handleAccessibilityAction : undefined}
203
+ disabled={isDeletedReplyRootMessage}
167
204
  >
168
205
  <Animated.View style={[styles.message, animatedBackgroundColor]}>
169
206
  {!message.mine && (
@@ -171,7 +208,7 @@ export function Message({
171
208
  {renderAuthor ? (
172
209
  <Avatar
173
210
  size={'md'}
174
- sourceUri={message.author.avatar}
211
+ sourceUri={author.avatar}
175
212
  style={styles.avatar}
176
213
  maxFontSizeMultiplier={1}
177
214
  minFontSizeMultiplier={1}
@@ -181,31 +218,41 @@ export function Message({
181
218
  ) : (
182
219
  <View style={styles.avatarPlaceholder} />
183
220
  )}
184
- <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
221
+ {repliesEnabled && (
222
+ <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
223
+ )}
185
224
  </View>
186
225
  )}
187
226
  <View style={[styles.messageContent, { marginBottom: messageBottomMargin }]}>
188
- {renderAuthor && (
227
+ {renderAuthor && !isDeletedReplyRootMessage && (
189
228
  <Text variant="footnote" style={styles.authorName}>
190
- {message.author.name}
229
+ {author.name}
191
230
  </Text>
192
231
  )}
193
232
  <View
194
233
  style={styles.messageBubble}
195
234
  onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
196
235
  >
197
- <ErrorBoundary>
198
- <MessageAttachments
199
- attachments={message.attachments}
200
- metaProps={metaProps}
201
- onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
202
- onMessageLongPress={handleMessageLongPress}
203
- />
204
- </ErrorBoundary>
205
- {messageText && (
236
+ {isDeletedReplyRootMessage ? (
206
237
  <View style={styles.messageText}>
207
- <MessageMarkdown text={messageText} />
238
+ <Text style={styles.replyRootDeletedText}>{messageText}</Text>
208
239
  </View>
240
+ ) : (
241
+ <>
242
+ <ErrorBoundary>
243
+ <MessageAttachments
244
+ attachments={attachments}
245
+ metaProps={metaProps}
246
+ onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
247
+ onMessageLongPress={handleMessageLongPress}
248
+ />
249
+ </ErrorBoundary>
250
+ {text && (
251
+ <View style={styles.messageText}>
252
+ <MessageMarkdown text={text} />
253
+ </View>
254
+ )}
255
+ </>
209
256
  )}
210
257
  </View>
211
258
  {showReplyCountButton && (
@@ -219,7 +266,7 @@ export function Message({
219
266
  {replyCountText}
220
267
  </TextButton>
221
268
  )}
222
- {hasReactions && (
269
+ {hasReactions && !isDeletedReplyRootMessage && (
223
270
  <View style={styles.messageReactions}>
224
271
  {reactionCounts.map(reaction => (
225
272
  <MessageReaction
@@ -232,7 +279,7 @@ export function Message({
232
279
  ))}
233
280
  </View>
234
281
  )}
235
- {showMessageMeta && (
282
+ {showMessageMeta && !isDeletedReplyRootMessage && (
236
283
  <View style={styles.messageMeta}>
237
284
  {message.mine && !pending && !error && (
238
285
  <MessageReadReceipts
@@ -282,7 +329,7 @@ export function Message({
282
329
  </View>
283
330
  )}
284
331
  </View>
285
- {message.mine && (
332
+ {repliesEnabled && message.mine && (
286
333
  <MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
287
334
  )}
288
335
  </Animated.View>
@@ -360,5 +407,9 @@ const useMessageStyles = ({ mine }: MessageResource) => {
360
407
  errorText: {
361
408
  color: colors.statusErrorText,
362
409
  },
410
+ replyRootDeletedText: {
411
+ color: colors.textColorDefaultSecondary,
412
+ fontStyle: 'italic',
413
+ },
363
414
  })
364
415
  }
@@ -1,9 +1,4 @@
1
- import {
2
- RouteProp,
3
- useNavigation,
4
- useTheme as useNavigationTheme,
5
- useRoute,
6
- } from '@react-navigation/native'
1
+ import { useNavigation, useTheme as useNavigationTheme, useRoute } from '@react-navigation/native'
7
2
  import React, { useCallback, useContext, useEffect, useState } from 'react'
8
3
  import {
9
4
  Platform,
@@ -30,10 +25,10 @@ import { ChatContext } from '../../contexts/chat_context'
30
25
  import { Haptic, ImagePicker, ImagePickerResult } from '../../utils/native_adapters'
31
26
  import {
32
27
  MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
33
- REPLIES_FEATURE_ENABLED,
34
28
  platformFontWeightMedium,
35
29
  platformPressedOpacityStyle,
36
30
  } from '../../utils'
31
+ import { availableFeatures, useFeatures } from '../../hooks/use_features'
37
32
  import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
38
33
  import { useMessageDraft } from '../../hooks/use_message_draft'
39
34
  import {
@@ -319,9 +314,7 @@ function MessageFormInput() {
319
314
  React.useContext(MessageFormContext)
320
315
  const attachmentError = attachmentUploader?.errorMessage
321
316
 
322
- const route = useRoute<RouteProp<ConversationScreenProps['route']>>()
323
- const conversationId = route.params.conversation_id
324
- const broadcastTypingStatus = useBroadcastTypingStatus(conversationId)
317
+ const broadcastTypingStatus = useBroadcastTypingStatus()
325
318
 
326
319
  const handleTextChange = (newText: string) => {
327
320
  setText(newText)
@@ -566,9 +559,11 @@ function EditingIndicator() {
566
559
  function ReplyIndicator() {
567
560
  const { replyRootId, replyRootAuthorFirstName } = React.useContext(MessageFormContext)
568
561
  const navigation = useNavigation()
562
+ const { featureEnabled } = useFeatures()
563
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
569
564
  const title = replyRootAuthorFirstName ? `Reply to ${replyRootAuthorFirstName}` : 'Reply'
570
565
 
571
- if (!REPLIES_FEATURE_ENABLED || !replyRootId) return null
566
+ if (!repliesEnabled || !replyRootId) return null
572
567
 
573
568
  return (
574
569
  <FormIndicatorRow
@@ -0,0 +1,69 @@
1
+ import { StyleSheet, View, type ViewStyle } from 'react-native'
2
+ import { useTheme } from '../../hooks'
3
+ import { Text } from '../display'
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
5
+ import { useFeatures } from '../../hooks/use_features'
6
+ import { availableFeatures } from '../../hooks/use_features'
7
+
8
+ export const LeaderMessagesDisabledBanner = () => {
9
+ const { featureEnabled } = useFeatures()
10
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
11
+
12
+ const description = repliesEnabled
13
+ ? 'Only leaders can send messages in this conversation.'
14
+ : 'Replies are frozen for everyone else.'
15
+
16
+ return <MessagesDisabledBanner description={description} />
17
+ }
18
+
19
+ export const MemberMessagesDisabledBanner = () => {
20
+ const styles = useStyles()
21
+ const { featureEnabled } = useFeatures()
22
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
23
+
24
+ const description = repliesEnabled
25
+ ? 'Only leaders can send messages, but you can still add reactions.'
26
+ : 'Replies have been disabled by a leader, but you can still add reactions.'
27
+
28
+ return <MessagesDisabledBanner description={description} style={styles.memberBanner} />
29
+ }
30
+
31
+ interface MessagesDisabledBannerProps {
32
+ description: string
33
+ style?: ViewStyle
34
+ }
35
+
36
+ const MessagesDisabledBanner = ({ description, style }: MessagesDisabledBannerProps) => {
37
+ const styles = useStyles()
38
+
39
+ return (
40
+ <View style={[styles.baseBanner, style]}>
41
+ <Text style={styles.text} variant="tertiary">
42
+ {description}
43
+ </Text>
44
+ </View>
45
+ )
46
+ }
47
+
48
+ const useStyles = () => {
49
+ const { colors } = useTheme()
50
+ const { bottom } = useSafeAreaInsets()
51
+
52
+ return StyleSheet.create({
53
+ baseBanner: {
54
+ paddingHorizontal: 16,
55
+ paddingVertical: 4,
56
+ backgroundColor: colors.statusNeutralComposedBackground,
57
+ borderTopWidth: 1,
58
+ borderTopColor: colors.borderColorDefaultBase,
59
+ },
60
+ text: {
61
+ textAlign: 'center',
62
+ },
63
+ memberBanner: {
64
+ paddingTop: 16,
65
+ paddingBottom: 16 + bottom,
66
+ marginBottom: -bottom,
67
+ },
68
+ })
69
+ }
@@ -6,7 +6,6 @@ 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 '../../utils'
10
9
 
11
10
  const MY_REPLY_CONNECTOR_WIDTH = 38
12
11
  const CONNECTOR_BORDER_WIDTH = 4
@@ -22,7 +21,6 @@ export function TheirReplyConnector({ message, messageBubbleHeight }: ReplyConne
22
21
  const styles = useStyles()
23
22
  const { nextRendersAuthor, threadPosition, renderAuthor, isReplyShadowMessage } = message
24
23
 
25
- if (!REPLIES_FEATURE_ENABLED) return null
26
24
  if (messageBubbleHeight === 0) return null // Prevents UI shifting
27
25
 
28
26
  const connectorMap: Record<string, React.ReactNode | null> = {
@@ -59,7 +57,6 @@ export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnecto
59
57
  const { nextRendersAuthor, threadPosition, prevIsMyReply, nextIsMyReply, isReplyShadowMessage } =
60
58
  message
61
59
 
62
- if (!REPLIES_FEATURE_ENABLED) return null
63
60
  if (messageBubbleHeight === 0) return null // Prevents UI shifting
64
61
 
65
62
  const connectorMap: Record<string, React.ReactNode | null> = {
@@ -25,6 +25,8 @@ import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
25
25
  import { assertKeysAreNumbers, pluralize } from '../../utils'
26
26
  import { useNavigation } from '@react-navigation/native'
27
27
  import { useConversationMessage } from '../../hooks/use_conversation_message'
28
+ import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
29
+ import { some } from 'lodash'
28
30
 
29
31
  interface ReplyShadowMessageProps extends MessageResource {
30
32
  messageId: string
@@ -61,10 +63,11 @@ interface ShadowMessageContentProps extends MessageResource {
61
63
  }
62
64
 
63
65
  function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageContentProps) {
64
- const { text, deletedAt } = message
66
+ const { text, deletedAt, author, attachments } = message
65
67
  const styles = useStyles(message)
66
68
  const { colors } = useTheme()
67
69
  const navigation = useNavigation()
70
+ const timestamp = useLiveRelativeTime(message.createdAt)
68
71
 
69
72
  const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
70
73
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
@@ -82,12 +85,16 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
82
85
  })
83
86
  }
84
87
 
88
+ const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : ''
89
+ const accessibilityLabel = `${author?.name || ''} Reply Preview ${attachmentLabel} ${messageText || ''} ${timestamp} ${replyCountText}`
90
+
85
91
  return (
86
92
  <Pressable
87
93
  android_ripple={{ color: colors.androidRippleNeutral }}
88
94
  onPress={handleNavigateToReplies}
89
95
  onPressIn={handleMessagePressIn}
90
96
  onPressOut={handleMessagePressOut}
97
+ accessibilityLabel={accessibilityLabel}
91
98
  accessibilityHint="Navigate to reply screen for this message"
92
99
  accessibilityRole="link"
93
100
  >
@@ -112,7 +119,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
112
119
  style={styles.messageBubble}
113
120
  onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
114
121
  >
115
- <MessageAttachmentImagery attachments={message.attachments} />
122
+ <MessageAttachmentImagery attachments={attachments} />
116
123
  {text && (
117
124
  <Text
118
125
  variant="footnote"
@@ -41,16 +41,12 @@ const TypingDots = () => {
41
41
  )
42
42
  }
43
43
 
44
- interface TypingIndicatorProps {
45
- conversationId: number
46
- }
47
-
48
44
  /**
49
45
  * Component to display typing indicators in a conversation
50
46
  * Shows "X is typing..." with animated dots
51
47
  */
52
- export const TypingIndicator = ({ conversationId }: TypingIndicatorProps) => {
53
- const typingPeople = useTypingIndicators(conversationId)
48
+ export const TypingIndicator = () => {
49
+ const typingPeople = useTypingIndicators()
54
50
  const styles = useStyles()
55
51
  const enabled = typingPeople.length > 0
56
52
 
@@ -2,6 +2,7 @@ import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'reac
2
2
  import {
3
3
  AccessibilityActionEvent,
4
4
  Platform,
5
+ Pressable,
5
6
  StyleProp,
6
7
  StyleSheet,
7
8
  View,
@@ -10,7 +11,6 @@ import {
10
11
  import ReanimatedSwipeable, {
11
12
  SwipeableMethods,
12
13
  } from 'react-native-gesture-handler/ReanimatedSwipeable'
13
- import { Pressable } from 'react-native-gesture-handler'
14
14
  import { useConversationsContext } from '../../contexts/conversations_context'
15
15
  import { useTheme, useCreateAndroidRippleColor } from '../../hooks'
16
16
  import {
@@ -1,7 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
- import { FlashList } from '@shopify/flash-list'
3
2
  import React, { useMemo } from 'react'
4
- import { StyleSheet, View } from 'react-native'
3
+ import { FlatList, StyleSheet, View } from 'react-native'
5
4
  import { useConversationsContext } from '../../contexts/conversations_context'
6
5
  import { useTheme } from '../../hooks'
7
6
  import { ConversationResource } from '../../types'
@@ -35,7 +34,7 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
35
34
 
36
35
  const showBadges = !chat_group_graph_id
37
36
 
38
- const data: FlashListItem[] = useMemo(() => {
37
+ const data: FlatListItem[] = useMemo(() => {
39
38
  if (isLoading) {
40
39
  return loadingPlaceholder
41
40
  }
@@ -54,9 +53,8 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
54
53
 
55
54
  return (
56
55
  <View style={styles.container}>
57
- <FlashList
56
+ <FlatList
58
57
  data={data}
59
- estimatedItemSize={97}
60
58
  keyExtractor={item => item.id.toString()}
61
59
  contentContainerStyle={styles.contentContainer}
62
60
  onRefresh={refetch}
@@ -114,18 +112,18 @@ const useStyles = () => {
114
112
  })
115
113
  }
116
114
 
117
- interface FlashListLoadingItem {
115
+ interface FlatListLoadingItem {
118
116
  type: 'loading'
119
117
  id: string
120
118
  }
121
- interface FlashListConversationItem {
119
+ interface FlatListConversationItem {
122
120
  type: 'conversation'
123
121
  resource: ConversationResource
124
122
  id: number
125
123
  }
126
- type FlashListItem = FlashListLoadingItem | FlashListConversationItem
124
+ type FlatListItem = FlatListLoadingItem | FlatListConversationItem
127
125
 
128
- const loadingPlaceholder: FlashListItem[] = Array.from({ length: 5 }, (_, i) => ({
126
+ const loadingPlaceholder: FlatListItem[] = Array.from({ length: 5 }, (_, i) => ({
129
127
  type: 'loading',
130
128
  id: `loading${i}`,
131
129
  }))
@@ -0,0 +1,34 @@
1
+ import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react'
2
+
3
+ interface ConversationContextValue {
4
+ conversationId: number
5
+ currentPageReplyRootId: string | null
6
+ }
7
+
8
+ interface ConversationContextProviderProps extends PropsWithChildren {
9
+ conversationId: number
10
+ currentPageReplyRootId: string | null
11
+ }
12
+
13
+ const ConversationContext = createContext<ConversationContextValue>({
14
+ conversationId: 0,
15
+ currentPageReplyRootId: null,
16
+ })
17
+
18
+ export const ConversationContextProvider = ({
19
+ children,
20
+ conversationId,
21
+ currentPageReplyRootId,
22
+ }: ConversationContextProviderProps) => {
23
+ const value = useMemo(
24
+ () => ({
25
+ conversationId,
26
+ currentPageReplyRootId,
27
+ }),
28
+ [conversationId, currentPageReplyRootId]
29
+ )
30
+
31
+ return <ConversationContext.Provider value={value}>{children}</ConversationContext.Provider>
32
+ }
33
+
34
+ export const useConversationContext = () => useContext(ConversationContext)
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useRef } from 'react'
2
2
  import { useApiClient } from './use_api_client'
3
+ import { useConversationContext } from '../contexts/conversation_context'
3
4
 
4
5
  const THROTTLE_INTERVAL = 3000 // 3 seconds
5
6
 
@@ -10,7 +11,8 @@ const THROTTLE_INTERVAL = 3000 // 3 seconds
10
11
  * after receiving a typing event. This is how we can show a steady typing indicator even
11
12
  * if the user types once every 2.9 seconds.
12
13
  */
13
- export const useBroadcastTypingStatus = (conversationId: string | number) => {
14
+ export const useBroadcastTypingStatus = () => {
15
+ const { conversationId, currentPageReplyRootId } = useConversationContext()
14
16
  const apiClient = useApiClient()
15
17
  const lastBroadcastTime = useRef<number>(0)
16
18
 
@@ -26,12 +28,14 @@ export const useBroadcastTypingStatus = (conversationId: string | number) => {
26
28
  apiClient.chat
27
29
  .post({
28
30
  url: `/me/conversations/${conversationId}/broadcast_typing_status`,
29
- data: { data: { type: 'TypingStatus', attributes: {} } },
31
+ data: {
32
+ data: { type: 'TypingStatus', attributes: { reply_root_id: currentPageReplyRootId } },
33
+ },
30
34
  })
31
35
  .catch(error => {
32
36
  console.error('Failed to broadcast typing status:', error)
33
37
  })
34
- }, [apiClient, conversationId])
38
+ }, [apiClient.chat, conversationId, currentPageReplyRootId])
35
39
 
36
40
  return broadcastTypingStatus
37
41
  }
@@ -16,7 +16,9 @@ export const useConversationMessages = (
16
16
  () =>
17
17
  data
18
18
  .filter(
19
- message => !message.deletedAt && (message.attachments?.length || message.text?.length)
19
+ message =>
20
+ (!message.deletedAt || message.replyRootId) &&
21
+ (message.attachments?.length || message.text?.length)
20
22
  )
21
23
  .sort((a, b) => -a.id.localeCompare(b.id)),
22
24
  [data]