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

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 (133) 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 +28 -17
  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/typing_indicator.d.ts +1 -5
  16. package/build/components/conversation/typing_indicator.d.ts.map +1 -1
  17. package/build/components/conversation/typing_indicator.js +2 -2
  18. package/build/components/conversation/typing_indicator.js.map +1 -1
  19. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  20. package/build/components/conversations/conversation_actions.js +1 -2
  21. package/build/components/conversations/conversation_actions.js.map +1 -1
  22. package/build/components/conversations/conversations.d.ts.map +1 -1
  23. package/build/components/conversations/conversations.js +2 -3
  24. package/build/components/conversations/conversations.js.map +1 -1
  25. package/build/contexts/conversation_context.d.ts +13 -0
  26. package/build/contexts/conversation_context.d.ts.map +1 -0
  27. package/build/contexts/conversation_context.js +14 -0
  28. package/build/contexts/conversation_context.js.map +1 -0
  29. package/build/hooks/use_broadcast_typing_status.d.ts +1 -1
  30. package/build/hooks/use_broadcast_typing_status.d.ts.map +1 -1
  31. package/build/hooks/use_broadcast_typing_status.js +7 -3
  32. package/build/hooks/use_broadcast_typing_status.js.map +1 -1
  33. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  34. package/build/hooks/use_conversation_messages.js +2 -1
  35. package/build/hooks/use_conversation_messages.js.map +1 -1
  36. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  37. package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
  38. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  39. package/build/hooks/use_features.d.ts +9 -0
  40. package/build/hooks/use_features.d.ts.map +1 -0
  41. package/build/hooks/use_features.js +35 -0
  42. package/build/hooks/use_features.js.map +1 -0
  43. package/build/hooks/use_message_create_or_update.d.ts +0 -2
  44. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  45. package/build/hooks/use_message_create_or_update.js +10 -8
  46. package/build/hooks/use_message_create_or_update.js.map +1 -1
  47. package/build/hooks/use_typing_indicators.d.ts +1 -1
  48. package/build/hooks/use_typing_indicators.d.ts.map +1 -1
  49. package/build/hooks/use_typing_indicators.js +16 -3
  50. package/build/hooks/use_typing_indicators.js.map +1 -1
  51. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  52. package/build/screens/conversation_details_screen.js +9 -6
  53. package/build/screens/conversation_details_screen.js.map +1 -1
  54. package/build/screens/conversation_new/components/form_list.d.ts +2 -2
  55. package/build/screens/conversation_new/components/form_list.d.ts.map +1 -1
  56. package/build/screens/conversation_new/components/form_list.js +2 -3
  57. package/build/screens/conversation_new/components/form_list.js.map +1 -1
  58. package/build/screens/conversation_screen.d.ts +2 -1
  59. package/build/screens/conversation_screen.d.ts.map +1 -1
  60. package/build/screens/conversation_screen.js +41 -18
  61. package/build/screens/conversation_screen.js.map +1 -1
  62. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.d.ts.map +1 -1
  63. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js +2 -3
  64. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js.map +1 -1
  65. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
  66. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +2 -3
  67. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
  68. package/build/screens/message_actions_screen.js +4 -2
  69. package/build/screens/message_actions_screen.js.map +1 -1
  70. package/build/types/jolt_events/reaction_events.d.ts +1 -0
  71. package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
  72. package/build/types/jolt_events/reaction_events.js.map +1 -1
  73. package/build/types/jolt_events/typing_events.d.ts +1 -0
  74. package/build/types/jolt_events/typing_events.d.ts.map +1 -1
  75. package/build/types/jolt_events/typing_events.js.map +1 -1
  76. package/build/types/resources/feature_resource.d.ts +7 -0
  77. package/build/types/resources/feature_resource.d.ts.map +1 -0
  78. package/build/types/resources/feature_resource.js +2 -0
  79. package/build/types/resources/feature_resource.js.map +1 -0
  80. package/build/utils/cache/messages_cache.d.ts +9 -0
  81. package/build/utils/cache/messages_cache.d.ts.map +1 -0
  82. package/build/utils/cache/messages_cache.js +89 -0
  83. package/build/utils/cache/messages_cache.js.map +1 -0
  84. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  85. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  86. package/build/utils/cache/optimistically_create_message.js +6 -3
  87. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  88. package/build/utils/index.d.ts +0 -1
  89. package/build/utils/index.d.ts.map +1 -1
  90. package/build/utils/index.js +0 -1
  91. package/build/utils/index.js.map +1 -1
  92. package/build/utils/request/get_features.d.ts +11 -0
  93. package/build/utils/request/get_features.d.ts.map +1 -0
  94. package/build/utils/request/get_features.js +18 -0
  95. package/build/utils/request/get_features.js.map +1 -0
  96. package/package.json +2 -3
  97. package/src/components/conversation/message.tsx +42 -20
  98. package/src/components/conversation/message_form.tsx +6 -11
  99. package/src/components/conversation/messages_disabled_banners.tsx +69 -0
  100. package/src/components/conversation/reply_connectors.tsx +0 -3
  101. package/src/components/conversation/typing_indicator.tsx +2 -6
  102. package/src/components/conversations/conversation_actions.tsx +1 -1
  103. package/src/components/conversations/conversations.tsx +7 -9
  104. package/src/contexts/conversation_context.tsx +34 -0
  105. package/src/hooks/use_broadcast_typing_status.ts +7 -3
  106. package/src/hooks/use_conversation_messages.ts +3 -1
  107. package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
  108. package/src/hooks/use_features.ts +47 -0
  109. package/src/hooks/use_message_create_or_update.ts +10 -9
  110. package/src/hooks/use_typing_indicators.ts +15 -3
  111. package/src/screens/conversation_details_screen.tsx +9 -6
  112. package/src/screens/conversation_new/components/form_list.tsx +3 -5
  113. package/src/screens/conversation_screen.tsx +58 -20
  114. package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx +2 -4
  115. package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +2 -4
  116. package/src/screens/message_actions_screen.tsx +4 -2
  117. package/src/types/jolt_events/reaction_events.ts +1 -0
  118. package/src/types/jolt_events/typing_events.ts +1 -0
  119. package/src/types/resources/feature_resource.ts +6 -0
  120. package/src/utils/cache/messages_cache.ts +113 -0
  121. package/src/utils/cache/optimistically_create_message.ts +7 -2
  122. package/src/utils/index.ts +0 -1
  123. package/src/utils/request/get_features.ts +20 -0
  124. package/build/components/conversation/disabled_replies_banners.d.ts +0 -3
  125. package/build/components/conversation/disabled_replies_banners.d.ts.map +0 -1
  126. package/build/components/conversation/disabled_replies_banners.js +0 -41
  127. package/build/components/conversation/disabled_replies_banners.js.map +0 -1
  128. package/build/utils/replies_local_feature_flag.d.ts +0 -2
  129. package/build/utils/replies_local_feature_flag.d.ts.map +0 -1
  130. package/build/utils/replies_local_feature_flag.js +0 -3
  131. package/build/utils/replies_local_feature_flag.js.map +0 -1
  132. package/src/components/conversation/disabled_replies_banners.tsx +0 -58
  133. package/src/utils/replies_local_feature_flag.ts +0 -2
@@ -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]
@@ -1,22 +1,21 @@
1
1
  import { ApiCollection, MessageResource } from '../types'
2
2
  import { useJoltChannel, useJoltEvent } from './use_jolt'
3
- import {
4
- deleteRecordInPagesData,
5
- updateOrCreateRecordInPagesData,
6
- updateRecordInPagesData,
7
- } from '../utils'
3
+ import { deleteRecordInPagesData } from '../utils'
8
4
  import { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'
9
5
  import { InfiniteData, useQueryClient } from '@tanstack/react-query'
10
6
  import { useCurrentPerson } from './use_current_person'
11
7
  import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
12
8
  import { getRequestQueryKey } from './use_suspense_api'
13
9
  import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'
14
- import { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'
15
10
  import { getMessagesRequestArgs } from '../utils/request/get_messages'
16
11
  import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
17
- import { isTemporaryMessageId } from './use_message_create_or_update'
18
12
  import { completeMessageCreationTracking } from '../utils/performance_tracking'
19
13
  import { useApiClient } from './use_api_client'
14
+ import {
15
+ updateCacheWithMessage,
16
+ updateCacheWithReaction,
17
+ getThreadedMessagesQueryKey,
18
+ } from '../utils/cache/messages_cache'
20
19
 
21
20
  interface Props {
22
21
  conversationId: number
@@ -47,42 +46,17 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
47
46
  }
48
47
  }
49
48
 
50
- queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {
51
- if (e.event === 'message.created') {
52
- // Before adding the new message, remove any pending temporary messages
53
- // with matching text to prevent duplicates from race conditions
54
- let dataAfterTempRemoval = prev
55
- if (prev && message.text && message.mine) {
56
- dataAfterTempRemoval = deleteRecordInPagesData({
57
- data: prev,
58
- record: message,
59
- matchFn: (existingMessage, _record) => {
60
- return (
61
- isTemporaryMessageId(existingMessage.id) &&
62
- existingMessage.text === message.text &&
63
- existingMessage.mine
64
- )
65
- },
66
- })
67
- }
68
-
69
- return updateOrCreateRecordInPagesData({
70
- data: dataAfterTempRemoval,
71
- record: message,
72
- processRecord: (record, current) => {
73
- return { ...current, ...record }
74
- },
75
- })
76
- } else {
77
- return updateRecordInPagesData({
78
- data: prev,
79
- record: message,
80
- processRecord: (record, current) => {
81
- return { ...current, ...record }
82
- },
83
- })
84
- }
85
- })
49
+ // Update the main conversation cache
50
+ updateCacheWithMessage(queryClient, messagesQueryKey, message, e.event)
51
+
52
+ // If message has a reply_root_id, also update the threaded cache
53
+ if (data.reply_root_id) {
54
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
55
+ conversationId,
56
+ data.reply_root_id
57
+ )
58
+ updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
59
+ }
86
60
  }
87
61
 
88
62
  const handleMessageDeleted = async (e: MessageDeletedEvent) => {
@@ -92,50 +66,34 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
92
66
  currentPersonId: currentPerson.id,
93
67
  })
94
68
 
69
+ // Update the main conversation cache
95
70
  queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>
96
71
  deleteRecordInPagesData({ data: prev, record: message })
97
72
  )
73
+
74
+ // If message has a reply_root_id, also update the threaded cache
75
+ if (data.reply_root_id) {
76
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
77
+ conversationId,
78
+ data.reply_root_id
79
+ )
80
+ queryClient.setQueryData<QueryData>(threadedMessagesQueryKey, prev =>
81
+ deleteRecordInPagesData({ data: prev, record: message })
82
+ )
83
+ }
98
84
  }
99
85
 
100
86
  const handleReactionJoltEvent = async (e: JoltReactionEvent) => {
101
- const { data } = e.data
102
- const message = { id: data.message_sort_key } as MessageResource
103
- queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>
104
- updateRecordInPagesData({
105
- data: prev,
106
- record: message,
107
- processRecord: (record, oldMessage) => {
108
- const reactionCounts = oldMessage.reactionCounts || []
109
- let foundMatch = false
110
- let newReactionCounts = reactionCounts.map(reactionCount => {
111
- if (reactionCount.value === data.value) {
112
- foundMatch = true
113
- return transformReactionEventDataToReactionCountResource({
114
- data,
115
- oldData: reactionCount,
116
- event: e.event,
117
- currentPersonId: currentPerson.id,
118
- })
119
- }
120
- return reactionCount
121
- })
122
-
123
- if (!foundMatch) {
124
- const newReactionCount = transformReactionEventDataToReactionCountResource({
125
- data,
126
- event: e.event,
127
- currentPersonId: currentPerson.id,
128
- })
129
-
130
- if (newReactionCount?.count) {
131
- newReactionCounts = [...newReactionCounts, newReactionCount]
132
- }
133
- }
134
-
135
- return { ...oldMessage, reactionCounts: newReactionCounts }
136
- },
137
- })
138
- )
87
+ // Update the main conversation cache and capture the reply_root_id if present
88
+ updateCacheWithReaction(queryClient, messagesQueryKey, e, currentPerson.id)
89
+
90
+ const replyRootId = e.data.data.reply_root_id
91
+
92
+ // If the message has a reply_root_id, also update the threaded cache
93
+ if (replyRootId) {
94
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(conversationId, replyRootId)
95
+ updateCacheWithReaction(queryClient, threadedMessagesQueryKey, e, currentPerson.id)
96
+ }
139
97
  }
140
98
 
141
99
  const handleTypingEvent = async (e: JoltTypingEvent) => {
@@ -0,0 +1,47 @@
1
+ import { useSuspenseQuery } from '@tanstack/react-query'
2
+ import { useCallback } from 'react'
3
+ import { getFeaturesRequestArgs, getFeaturesQueryKey } from '../utils/request/get_features'
4
+ import { useApiClient } from './use_api_client'
5
+ import type { FeatureResource } from '../types/resources/feature_resource'
6
+ import { ApiCollection } from '../types'
7
+
8
+ export function useFeatures() {
9
+ const apiClient = useApiClient()
10
+ const requestArgs = getFeaturesRequestArgs()
11
+
12
+ const { data } = useSuspenseQuery({
13
+ queryKey: getFeaturesQueryKey(),
14
+ queryFn: () => {
15
+ return apiClient.chat
16
+ .get<ApiCollection<FeatureResource>>(requestArgs)
17
+ .catch(() => stableEmptyFeatures)
18
+ },
19
+ staleTime: 1000 * 60 * 5, // 5 minutes
20
+ })
21
+
22
+ const features = data.data
23
+
24
+ const featureEnabled = useCallback(
25
+ (featureName: string) =>
26
+ features.some(feature => feature.name === featureName && feature.enabled),
27
+ [features]
28
+ )
29
+
30
+ return {
31
+ features,
32
+ featureEnabled,
33
+ }
34
+ }
35
+
36
+ export const availableFeatures = {
37
+ threaded_replies: 'QA_MOBILE_threaded_replies',
38
+ }
39
+
40
+ const stableEmptyFeatures: ApiCollection<FeatureResource> = {
41
+ data: [],
42
+ links: {},
43
+ meta: {
44
+ count: 0,
45
+ totalCount: 0,
46
+ },
47
+ }
@@ -14,6 +14,7 @@ import { useCurrentPerson } from './use_current_person'
14
14
  import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'
15
15
  import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'
16
16
  import { startMessageCreationTracking } from '../utils/performance_tracking'
17
+ import { isNewMessage } from '../utils/cache/messages_cache'
17
18
 
18
19
  interface Props {
19
20
  conversationId: number
@@ -93,6 +94,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
93
94
  text,
94
95
  attachments,
95
96
  currentPerson,
97
+ replyRootId,
96
98
  })
97
99
 
98
100
  return { message: optimisticMessage }
@@ -103,7 +105,10 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
103
105
 
104
106
  // Add error to the optimistic message from the cache on error
105
107
  if (optimisticMessage) {
106
- const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
108
+ const queryKey = getMessagesQueryKey({
109
+ conversation_id: conversationId,
110
+ reply_root_id: replyRootId,
111
+ })
107
112
  chatQueryClient.setQueryData(
108
113
  queryKey,
109
114
  (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>
@@ -123,7 +128,10 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
123
128
  const { message: optimisticMessage } = context || {}
124
129
  const updatedMessage = result.data
125
130
  type QueryData = InfiniteData<ApiCollection<MessageResource>>
126
- const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
131
+ const queryKey = getMessagesQueryKey({
132
+ conversation_id: conversationId,
133
+ reply_root_id: replyRootId,
134
+ })
127
135
 
128
136
  // First remove the optimistic message if it exists
129
137
  if (optimisticMessage) {
@@ -148,13 +156,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
148
156
  return mutation
149
157
  }
150
158
 
151
- export function isTemporaryMessageId(messageId?: string | null): boolean {
152
- return !!messageId && messageId.endsWith('-temp')
153
- }
154
- export function isNewMessage(message?: MessageResource): boolean {
155
- return !message?.id || isTemporaryMessageId(message.id)
156
- }
157
-
158
159
  /**
159
160
  * Generate a random UUID (v4) for idempotent keys.
160
161
  * Uses Math.random, which is not cryptographically secure.
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
2
2
  import { useMemo } from 'react'
3
3
  import { PersonTyping } from './use_typing_status_cache'
4
4
  import { useCurrentPerson } from './use_current_person'
5
+ import { useConversationContext } from '../contexts/conversation_context'
5
6
 
6
7
  /**
7
8
  * Hook for getting people currently typing in a conversation
@@ -9,9 +10,12 @@ import { useCurrentPerson } from './use_current_person'
9
10
  * The query function itself doesn't do anything, but we add to the cache
10
11
  * from useTypingStatusCache when we receive a typing event.
11
12
  */
12
- export const useTypingIndicators = (conversationId: number) => {
13
+ export const useTypingIndicators = () => {
14
+ const { conversationId, currentPageReplyRootId } = useConversationContext()
13
15
  const cacheKey = useMemo(() => ['conversationTyping', String(conversationId)], [conversationId])
14
16
  const currentPerson = useCurrentPerson()
17
+ const isCurrentPerson = (authorId: number) => authorId === currentPerson?.id
18
+ const inMainConversationView = !currentPageReplyRootId
15
19
  const now = Date.now()
16
20
  const { data } = useQuery({
17
21
  queryKey: cacheKey,
@@ -19,8 +23,16 @@ export const useTypingIndicators = (conversationId: number) => {
19
23
  initialData: stableArray,
20
24
  })
21
25
 
22
- // Filter out expired entries and the current user
23
- return data.filter(person => person.expires > now && person.author_id !== currentPerson?.id)
26
+ return data.filter(({ author_id, expires, reply_root_id }) => {
27
+ if (isCurrentPerson(author_id)) return false
28
+ if (now > expires) return false
29
+
30
+ // If you are in the main conversation view, you will see any message sent
31
+ if (inMainConversationView) return true
32
+
33
+ // If you are in a reply view, you will only see messages sent in this reply thread
34
+ return reply_root_id === currentPageReplyRootId
35
+ })
24
36
  }
25
37
 
26
38
  /**
@@ -9,6 +9,7 @@ import React, {
9
9
  useRef,
10
10
  } from 'react'
11
11
  import {
12
+ FlatList,
12
13
  StyleSheet,
13
14
  TextInput,
14
15
  View,
@@ -40,10 +41,11 @@ import {
40
41
  } from '../hooks/use_conversation'
41
42
  import { MemberResource, isDefined } from '../types'
42
43
  import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
43
- import { FlashList } from '@shopify/flash-list'
44
44
  import { tokens } from '../vendor/tapestry/tokens'
45
45
  import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
46
46
  import { GroupResource } from '../types/resources/group_resource'
47
+ import { useFeatures } from '../hooks/use_features'
48
+ import { availableFeatures } from '../hooks/use_features'
47
49
 
48
50
  // =========================================
49
51
  // ====== Factory Constants & Types ========
@@ -93,6 +95,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
93
95
  const { repliesDisabled, setRepliesDisabled } = useConversationDisableReplies(route.params)
94
96
  const { mutate: saveTitle } = useConversationUpdate(route.params)
95
97
  const { mutate: deleteConversation } = useConversationDelete(route.params)
98
+ const { featureEnabled } = useFeatures()
99
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
96
100
 
97
101
  const trimmedTitle = title.trim()
98
102
  const emptyTitle = trimmedTitle === '' || title === null
@@ -250,8 +254,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
250
254
  {
251
255
  type: canUpdate ? SectionTypes.setting : SectionTypes.hidden,
252
256
  data: {
253
- title: 'Freeze conversation',
254
- subtitle: 'Disables replies for everyone except leaders.',
257
+ title: repliesEnabled ? 'Leader messages only' : 'Freeze conversation',
258
+ subtitle: repliesEnabled ? undefined : 'Disables replies for everyone except leaders.',
255
259
  rightItem: (
256
260
  <Switch value={repliesDisabled} onChange={() => setRepliesDisabled(!repliesDisabled)} />
257
261
  ),
@@ -300,9 +304,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
300
304
 
301
305
  return (
302
306
  <View style={styles.listContainer}>
303
- <FlashList
307
+ <FlatList
304
308
  data={listData as SectionListData}
305
- estimatedItemSize={52}
306
309
  contentContainerStyle={styles.contentContainer}
307
310
  renderItem={({ item, index }) => {
308
311
  const [isStart, isEnd] = [
@@ -375,7 +378,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
375
378
  return null
376
379
  }
377
380
  }}
378
- onEndReached={fetchNextPageOfMembers}
381
+ onEndReached={() => fetchNextPageOfMembers()}
379
382
  />
380
383
  </View>
381
384
  )
@@ -1,6 +1,5 @@
1
1
  import React from 'react'
2
- import { StyleSheet, View } from 'react-native'
3
- import { FlashList, type FlashListProps } from '@shopify/flash-list'
2
+ import { FlatList, type FlatListProps, StyleSheet, View } from 'react-native'
4
3
  import { MemberResource } from '../../../types'
5
4
  import { Person, Text } from '../../../components/display'
6
5
  import { useTheme } from '../../../hooks'
@@ -8,7 +7,7 @@ import { useTheme } from '../../../hooks'
8
7
  interface FormListProps {
9
8
  memberData: MemberResource[]
10
9
  loadingMore?: boolean
11
- FormContent?: FlashListProps<MemberResource>['ListHeaderComponent']
10
+ FormContent?: FlatListProps<MemberResource>['ListHeaderComponent']
12
11
  listEmptyText?: string
13
12
  onEndReached?: () => void
14
13
  }
@@ -23,7 +22,7 @@ export const FormList = ({
23
22
  const styles = useStyles()
24
23
 
25
24
  return (
26
- <FlashList
25
+ <FlatList
27
26
  data={memberData}
28
27
  ListHeaderComponent={FormContent}
29
28
  renderItem={({ item }) => <Person person={item} style={styles.person} />}
@@ -31,7 +30,6 @@ export const FormList = ({
31
30
  loadingMore ? <Text style={styles.loadingMore}>Loading more...</Text> : null
32
31
  }
33
32
  keyExtractor={item => item.id.toString()}
34
- estimatedItemSize={45}
35
33
  ListEmptyComponent={<ListEmptyText text={listEmptyText || 'No members found'} />}
36
34
  onEndReached={onEndReached}
37
35
  />