@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
@@ -14,9 +14,9 @@ import { FlatList, Platform, StyleSheet, View } from 'react-native'
14
14
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
15
15
  import { Badge, Icon, Text } from '../components'
16
16
  import {
17
- LeaderDisabledRepliesBanner,
18
- MemberDisabledRepliesBanner,
19
- } from '../components/conversation/disabled_replies_banners'
17
+ LeaderMessagesDisabledBanner,
18
+ MemberMessagesDisabledBanner,
19
+ } from '../components/conversation/messages_disabled_banners'
20
20
  import { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'
21
21
  import BlankState from '../components/primitive/blank_state_primitive'
22
22
  import { Message } from '../components/conversation/message'
@@ -35,7 +35,8 @@ 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
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
38
- import { REPLIES_FEATURE_ENABLED } from '../utils'
38
+ import { availableFeatures, useFeatures } from '../hooks/use_features'
39
+ import { ConversationContextProvider } from '../contexts/conversation_context'
39
40
 
40
41
  export type ConversationRouteProps = {
41
42
  conversation_id: number
@@ -53,6 +54,19 @@ export type ConversationRouteProps = {
53
54
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
54
55
 
55
56
  export function ConversationScreen({ route }: ConversationScreenProps) {
57
+ const { conversation_id, reply_root_id } = route.params
58
+
59
+ return (
60
+ <ConversationContextProvider
61
+ conversationId={conversation_id}
62
+ currentPageReplyRootId={reply_root_id ?? null}
63
+ >
64
+ <ConversationScreenContent route={route} />
65
+ </ConversationContextProvider>
66
+ )
67
+ }
68
+
69
+ function ConversationScreenContent({ route }: ConversationScreenProps) {
56
70
  const styles = useStyles()
57
71
  const navigation = useNavigation()
58
72
  const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
@@ -66,7 +80,13 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
66
80
  useConversationMessagesJoltEvents({ conversationId: conversation_id })
67
81
  useEnsureConversationsRouteExists()
68
82
  useMarkLatestMessageRead({ conversation, messages })
69
- const messagesWithSeparators = groupMessages({ ms: messages, inReplyScreen: !!reply_root_id })
83
+ const { featureEnabled } = useFeatures()
84
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
85
+ const messagesWithSeparators = groupMessages({
86
+ ms: messages,
87
+ inReplyScreen: !!reply_root_id,
88
+ repliesEnabled,
89
+ })
70
90
  const noMessages = messagesWithSeparators.length === 0
71
91
 
72
92
  const { repliesDisabled, memberAbility, badges, title } = conversation
@@ -164,6 +184,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
164
184
  conversation_id={conversation_id}
165
185
  latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
166
186
  inReplyScreen={!!reply_root_id}
187
+ repliesEnabled={repliesEnabled}
167
188
  />
168
189
  )
169
190
  }}
@@ -172,8 +193,8 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
172
193
  />
173
194
  )}
174
195
  <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
175
- {!noMessages && <TypingIndicator conversationId={conversation_id} />}
176
- {showLeaderDisabledReplyBanner && <LeaderDisabledRepliesBanner />}
196
+ {!noMessages && <TypingIndicator />}
197
+ {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
177
198
  {canReply ? (
178
199
  <MessageForm.Root
179
200
  replyRootAuthorFirstName={replyRootAuthorFirstName}
@@ -194,7 +215,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
194
215
  <MessageForm.SubmitButton />
195
216
  </MessageForm.Root>
196
217
  ) : (
197
- <MemberDisabledRepliesBanner />
218
+ <MemberMessagesDisabledBanner />
198
219
  )}
199
220
  </KeyboardView>
200
221
  </View>
@@ -252,9 +273,14 @@ type ReplyShadowMessage = {
252
273
  interface GroupMessagesProps {
253
274
  ms: MessageResource[]
254
275
  inReplyScreen?: boolean
276
+ repliesEnabled?: boolean
255
277
  }
256
278
 
257
- export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
279
+ export const groupMessages = ({
280
+ ms,
281
+ inReplyScreen,
282
+ repliesEnabled = false,
283
+ }: GroupMessagesProps) => {
258
284
  let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
259
285
  let encounteredOneOfMyMessages = false
260
286
 
@@ -284,6 +310,24 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
284
310
  message.replyRootId &&
285
311
  !threadRoot &&
286
312
  (prevMessageDifferentThread || prevMessageIsDateSeparator)
313
+ const lastInGroup =
314
+ !nextMessage ||
315
+ nextMessageDifferentAuthor ||
316
+ nextMessageMoreThan5Minutes ||
317
+ nextMessageDifferentThread ||
318
+ nextMessageIsDateSeparator
319
+ const renderAuthor =
320
+ !message.mine &&
321
+ (!prevMessage ||
322
+ prevMessageDifferentAuthor ||
323
+ prevMessageMoreThan5Minutes ||
324
+ prevMessageDifferentThread ||
325
+ prevMessageIsDateSeparator)
326
+ const nextIsReplyShadowMessage =
327
+ repliesEnabled &&
328
+ nextMessageInThread &&
329
+ !nextMessageThreadRoot &&
330
+ (nextMessageDifferentThread || nextMessageIsDateSeparator)
287
331
 
288
332
  if (message.mine && !encounteredOneOfMyMessages) {
289
333
  encounteredOneOfMyMessages = true
@@ -291,17 +335,12 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
291
335
  } else {
292
336
  message.myLatestInConversation = false
293
337
  }
294
- message.lastInGroup = !nextMessage || nextMessageDifferentAuthor || nextMessageMoreThan5Minutes
295
- message.renderAuthor =
296
- !message.mine && (!prevMessage || prevMessageDifferentAuthor || prevMessageMoreThan5Minutes)
338
+ message.lastInGroup = lastInGroup
339
+ message.renderAuthor = renderAuthor
297
340
  message.threadPosition = null
298
341
  message.nextRendersAuthor = nextMessage?.renderAuthor
299
342
  message.isReplyShadowMessage = false
300
- message.nextIsReplyShadowMessage =
301
- REPLIES_FEATURE_ENABLED &&
302
- nextMessageInThread &&
303
- !nextMessageThreadRoot &&
304
- (nextMessageDifferentThread || nextMessageIsDateSeparator)
343
+ message.nextIsReplyShadowMessage = nextIsReplyShadowMessage
305
344
 
306
345
  if (!inReplyScreen && inThread) {
307
346
  message.prevIsMyReply = prevMessage?.mine
@@ -314,13 +353,12 @@ export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
314
353
  message.threadPosition = null // ensures we don't render a connector for root replies that aren't immediately followed up a reply
315
354
  else if (firstInThread) message.threadPosition = 'first'
316
355
  else if (lastInThread) message.threadPosition = 'last'
317
- else if (!prevMessageDifferentThread && !nextMessageDifferentThread)
318
- message.threadPosition = 'center'
356
+ else message.threadPosition = 'center'
319
357
  }
320
358
 
321
359
  enrichedMessages.push(message)
322
360
 
323
- if (insertReplyShadowMessage && REPLIES_FEATURE_ENABLED) {
361
+ if (insertReplyShadowMessage && repliesEnabled) {
324
362
  enrichedMessages.push({
325
363
  type: 'ReplyShadowMessage',
326
364
  id: `${message.id}-${message.replyRootId}`,
@@ -1,7 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
- import { StyleSheet, View } from 'react-native'
4
- import { FlashList } from '@shopify/flash-list'
3
+ import { FlatList, StyleSheet, View } from 'react-native'
5
4
  import { Heading } from '../../components'
6
5
  import { GroupsGroupResource } from '../../types'
7
6
  import { useGroupsGroups } from '../../hooks/use_groups_groups'
@@ -31,10 +30,9 @@ export const ConversationSelectGroupRecipientsScreen = ({
31
30
  }
32
31
 
33
32
  return (
34
- <FlashList
33
+ <FlatList
35
34
  data={groupsWithCreatePermission}
36
35
  keyExtractor={item => item.id.toString()}
37
- estimatedItemSize={65}
38
36
  contentContainerStyle={styles.contentContainer}
39
37
  ListHeaderComponent={
40
38
  <View style={styles.sectionHeader}>
@@ -1,7 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
- import { StyleSheet, View } from 'react-native'
4
- import { FlashList } from '@shopify/flash-list'
3
+ import { FlatList, StyleSheet, View } from 'react-native'
5
4
  import { Heading } from '../../components'
6
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
7
6
  import { ConversationSelectRecipientsScreenProps } from './types/screen_props'
@@ -30,10 +29,9 @@ export const ConversationSelectTeamsILeadRecipientsScreen = ({
30
29
  }
31
30
 
32
31
  return (
33
- <FlashList
32
+ <FlatList
34
33
  data={serviceTypes}
35
34
  keyExtractor={item => item.id.toString()}
36
- estimatedItemSize={65}
37
35
  contentContainerStyle={styles.contentContainer}
38
36
  ListHeaderComponent={
39
37
  <View style={styles.sectionHeader}>
@@ -14,7 +14,7 @@ import { useMessageReactionToggle } from '../hooks/use_message_reaction_toggle'
14
14
  import { ReactionCountResource } from '../types/resources/reaction'
15
15
  import { Clipboard, Haptic } from '../utils/native_adapters'
16
16
  import { MessageResource } from '../types'
17
- import { REPLIES_FEATURE_ENABLED } from '../utils'
17
+ import { availableFeatures, useFeatures } from '../hooks/use_features'
18
18
 
19
19
  export const MessageActionsScreenOptions = getFormSheetScreenOptions({
20
20
  sheetAllowedDetents: [0.5],
@@ -76,6 +76,8 @@ function MessageActionsScreenContent({
76
76
  const navigation = useNavigation()
77
77
  const apiClient = useApiClient()
78
78
  const styles = useStyles()
79
+ const { featureEnabled } = useFeatures()
80
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
79
81
 
80
82
  const myReactions = message?.reactionCounts
81
83
  .filter(reaction => reaction.mine)
@@ -190,7 +192,7 @@ function MessageActionsScreenContent({
190
192
  ))}
191
193
  </View>
192
194
  <View style={styles.actions}>
193
- {REPLIES_FEATURE_ENABLED && !inReplyScreen && (
195
+ {repliesEnabled && !inReplyScreen && (
194
196
  <FormSheet.Action
195
197
  onPress={handleReplyPress}
196
198
  title="Reply to message"
@@ -6,6 +6,7 @@ interface BaseReactionEventData extends Record<string, unknown> {
6
6
  author_id: number
7
7
  conversation_id: number
8
8
  message_sort_key: string
9
+ reply_root_id?: string | null
9
10
  created_at: string
10
11
  organization_id: number
11
12
  value: ReactionCountResource['value']
@@ -13,4 +13,5 @@ export interface TypingBroadcastDataAttributes {
13
13
  author_id: number
14
14
  author_name: string
15
15
  id: string
16
+ reply_root_id: string | null
16
17
  }
@@ -0,0 +1,6 @@
1
+ export interface FeatureResource {
2
+ type: 'Feature'
3
+ id: string
4
+ name: string
5
+ enabled: boolean
6
+ }
@@ -0,0 +1,113 @@
1
+ import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
+ import { ApiCollection, MessageResource } from '../../types'
3
+ import { deleteRecordInPagesData } from './page_mutations'
4
+ import { updateOrCreateRecordInPagesData, updateRecordInPagesData } from './page_mutations'
5
+ import { JoltReactionEvent } from '../../types/jolt_events'
6
+ import { transformReactionEventDataToReactionCountResource } from '../jolt/transform_reaction_event_data_to_reaction_count_resource'
7
+ import { getMessagesRequestArgs } from '../request/get_messages'
8
+ import { getRequestQueryKey } from '../../hooks/use_suspense_api'
9
+
10
+ export function updateCacheWithMessage(
11
+ queryClient: QueryClient,
12
+ queryKey: unknown[],
13
+ message: MessageResource,
14
+ event: 'message.created' | 'message.updated'
15
+ ) {
16
+ queryClient.setQueryData<MessagesQueryData>(queryKey, prev => {
17
+ if (event === 'message.created') {
18
+ // Before adding the new message, remove any pending temporary messages
19
+ // with matching text to prevent duplicates from race conditions
20
+ let dataAfterTempRemoval = prev
21
+ if (prev && message.text && message.mine) {
22
+ dataAfterTempRemoval = deleteRecordInPagesData({
23
+ data: prev,
24
+ record: message,
25
+ matchFn: (existingMessage, _record) => {
26
+ return (
27
+ isTemporaryMessageId(existingMessage.id) &&
28
+ existingMessage.text === message.text &&
29
+ existingMessage.mine
30
+ )
31
+ },
32
+ })
33
+ }
34
+
35
+ return updateOrCreateRecordInPagesData({
36
+ data: dataAfterTempRemoval,
37
+ record: message,
38
+ processRecord: (record, current) => {
39
+ return { ...current, ...record }
40
+ },
41
+ })
42
+ } else {
43
+ return updateRecordInPagesData({
44
+ data: prev,
45
+ record: message,
46
+ processRecord: (record, current) => {
47
+ return { ...current, ...record }
48
+ },
49
+ })
50
+ }
51
+ })
52
+ }
53
+
54
+ export function updateCacheWithReaction(
55
+ queryClient: QueryClient,
56
+ queryKey: unknown[],
57
+ event: JoltReactionEvent,
58
+ currentPersonId: number
59
+ ) {
60
+ const message = { id: event.data.data.message_sort_key } as MessageResource
61
+ queryClient.setQueryData<MessagesQueryData>(queryKey, prev =>
62
+ updateRecordInPagesData({
63
+ data: prev,
64
+ record: message,
65
+ processRecord: (record, oldMessage) => {
66
+ const reactionCounts = oldMessage.reactionCounts || []
67
+ let foundMatch = false
68
+ let newReactionCounts = reactionCounts.map(reactionCount => {
69
+ if (reactionCount.value === event.data.data.value) {
70
+ foundMatch = true
71
+ return transformReactionEventDataToReactionCountResource({
72
+ data: event.data.data,
73
+ oldData: reactionCount,
74
+ event: event.event,
75
+ currentPersonId,
76
+ })
77
+ }
78
+ return reactionCount
79
+ })
80
+
81
+ if (!foundMatch) {
82
+ const newReactionCount = transformReactionEventDataToReactionCountResource({
83
+ data: event.data.data,
84
+ event: event.event,
85
+ currentPersonId,
86
+ })
87
+
88
+ if (newReactionCount?.count) {
89
+ newReactionCounts = [...newReactionCounts, newReactionCount]
90
+ }
91
+ }
92
+
93
+ return { ...oldMessage, reactionCounts: newReactionCounts }
94
+ },
95
+ })
96
+ )
97
+ }
98
+
99
+ type MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>
100
+ export function isTemporaryMessageId(messageId?: string | null): boolean {
101
+ return !!messageId && messageId.endsWith('-temp')
102
+ }
103
+ export function isNewMessage(message?: MessageResource): boolean {
104
+ return !message?.id || isTemporaryMessageId(message.id)
105
+ }
106
+
107
+ export function getThreadedMessagesQueryKey(conversationId: number, replyRootId: string) {
108
+ const requestArgs = getMessagesRequestArgs({
109
+ conversation_id: conversationId,
110
+ reply_root_id: replyRootId,
111
+ })
112
+ return getRequestQueryKey(requestArgs)
113
+ }
@@ -14,12 +14,14 @@ export function optimisticallyCreateMessage({
14
14
  attachments,
15
15
  currentPerson,
16
16
  message,
17
+ replyRootId,
17
18
  }: {
18
19
  conversationId: number
19
20
  text: string
20
21
  attachments?: DenormalizedAttachmentResourceForCreate[]
21
22
  currentPerson: CurrentPersonResource
22
23
  message?: MessageResource
24
+ replyRootId?: string | null
23
25
  }) {
24
26
  const id = message?.id || generateTempMessageId()
25
27
 
@@ -49,12 +51,15 @@ export function optimisticallyCreateMessage({
49
51
  lastInGroup: true,
50
52
  pending: true,
51
53
  replyCount: 0,
52
- replyRootId: null,
54
+ replyRootId: replyRootId || null,
53
55
  }
54
56
 
55
57
  // Add the optimistic message to the cache
56
58
  type QueryData = InfiniteData<ApiCollection<MessageResource>>
57
- const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
59
+ const queryKey = getMessagesQueryKey({
60
+ conversation_id: conversationId,
61
+ reply_root_id: replyRootId,
62
+ })
58
63
 
59
64
  chatQueryClient.setQueryData<QueryData>(queryKey, data =>
60
65
  updateOrCreateRecordInPagesData({
@@ -9,4 +9,3 @@ export * from './pluralize'
9
9
  export * from './destructure_chat_group_graph_id'
10
10
  export * from './convert_attachments_for_create'
11
11
  export * from './assert_keys_are_numbers'
12
- export * from './replies_local_feature_flag'
@@ -0,0 +1,20 @@
1
+ import { getRequestQueryKey } from '../../hooks/use_suspense_api'
2
+
3
+ export const getFeaturesRequestArgs = () => {
4
+ const url = '/me/features'
5
+
6
+ return {
7
+ url,
8
+ data: {
9
+ perPage: 100,
10
+ fields: {
11
+ Feature: ['name', 'enabled'],
12
+ },
13
+ },
14
+ }
15
+ }
16
+
17
+ export const getFeaturesQueryKey = () => {
18
+ const requestArgs = getFeaturesRequestArgs()
19
+ return getRequestQueryKey(requestArgs)
20
+ }
@@ -1,3 +0,0 @@
1
- export declare const LeaderDisabledRepliesBanner: () => import("react").JSX.Element;
2
- export declare const MemberDisabledRepliesBanner: () => import("react").JSX.Element;
3
- //# sourceMappingURL=disabled_replies_banners.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"disabled_replies_banners.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/disabled_replies_banners.tsx"],"names":[],"mappings":"AAKA,eAAO,MAAM,2BAA2B,mCAEvC,CAAA;AAED,eAAO,MAAM,2BAA2B,mCAQvC,CAAA"}
@@ -1,41 +0,0 @@
1
- import { StyleSheet, View } from 'react-native';
2
- import { useTheme } from '../../hooks';
3
- import { Text } from '../display';
4
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
- export const LeaderDisabledRepliesBanner = () => {
6
- return <DisabledRepliesBanner description="Replies are frozen for everyone else."/>;
7
- };
8
- export const MemberDisabledRepliesBanner = () => {
9
- const styles = useStyles();
10
- return (<DisabledRepliesBanner description="Replies have been disabled by a leader, but you can still add reactions." style={styles.memberBanner}/>);
11
- };
12
- const DisabledRepliesBanner = ({ description, style }) => {
13
- const styles = useStyles();
14
- return (<View style={[styles.baseBanner, style]}>
15
- <Text style={styles.text} variant="tertiary">
16
- {description}
17
- </Text>
18
- </View>);
19
- };
20
- const useStyles = () => {
21
- const { colors } = useTheme();
22
- const { bottom } = useSafeAreaInsets();
23
- return StyleSheet.create({
24
- baseBanner: {
25
- paddingHorizontal: 16,
26
- paddingVertical: 4,
27
- backgroundColor: colors.statusNeutralComposedBackground,
28
- borderTopWidth: 1,
29
- borderTopColor: colors.borderColorDefaultBase,
30
- },
31
- text: {
32
- textAlign: 'center',
33
- },
34
- memberBanner: {
35
- paddingTop: 16,
36
- paddingBottom: 16 + bottom,
37
- marginBottom: -bottom,
38
- },
39
- });
40
- };
41
- //# sourceMappingURL=disabled_replies_banners.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"disabled_replies_banners.js","sourceRoot":"","sources":["../../../src/components/conversation/disabled_replies_banners.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAkB,MAAM,cAAc,CAAA;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAElE,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,EAAE;IAC9C,OAAO,CAAC,qBAAqB,CAAC,WAAW,CAAC,uCAAuC,EAAG,CAAA;AACtF,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,EAAE;IAC9C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,OAAO,CACL,CAAC,qBAAqB,CACpB,WAAW,CAAC,0EAA0E,CACtF,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,EAC3B,CACH,CAAA;AACH,CAAC,CAAA;AAOD,MAAM,qBAAqB,GAAG,CAAC,EAAE,WAAW,EAAE,KAAK,EAA8B,EAAE,EAAE;IACnF,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CACtC;MAAA,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAC1C;QAAA,CAAC,WAAW,CACd;MAAA,EAAE,IAAI,CACR;IAAA,EAAE,IAAI,CAAC,CACR,CAAA;AACH,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,GAAG,EAAE;IACrB,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAA;IAC7B,MAAM,EAAE,MAAM,EAAE,GAAG,iBAAiB,EAAE,CAAA;IAEtC,OAAO,UAAU,CAAC,MAAM,CAAC;QACvB,UAAU,EAAE;YACV,iBAAiB,EAAE,EAAE;YACrB,eAAe,EAAE,CAAC;YAClB,eAAe,EAAE,MAAM,CAAC,+BAA+B;YACvD,cAAc,EAAE,CAAC;YACjB,cAAc,EAAE,MAAM,CAAC,sBAAsB;SAC9C;QACD,IAAI,EAAE;YACJ,SAAS,EAAE,QAAQ;SACpB;QACD,YAAY,EAAE;YACZ,UAAU,EAAE,EAAE;YACd,aAAa,EAAE,EAAE,GAAG,MAAM;YAC1B,YAAY,EAAE,CAAC,MAAM;SACtB;KACF,CAAC,CAAA;AACJ,CAAC,CAAA","sourcesContent":["import { StyleSheet, View, type ViewStyle } from 'react-native'\nimport { useTheme } from '../../hooks'\nimport { Text } from '../display'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\n\nexport const LeaderDisabledRepliesBanner = () => {\n return <DisabledRepliesBanner description=\"Replies are frozen for everyone else.\" />\n}\n\nexport const MemberDisabledRepliesBanner = () => {\n const styles = useStyles()\n return (\n <DisabledRepliesBanner\n description=\"Replies have been disabled by a leader, but you can still add reactions.\"\n style={styles.memberBanner}\n />\n )\n}\n\ninterface DisabledRepliesBannerProps {\n description: string\n style?: ViewStyle\n}\n\nconst DisabledRepliesBanner = ({ description, style }: DisabledRepliesBannerProps) => {\n const styles = useStyles()\n\n return (\n <View style={[styles.baseBanner, style]}>\n <Text style={styles.text} variant=\"tertiary\">\n {description}\n </Text>\n </View>\n )\n}\n\nconst useStyles = () => {\n const { colors } = useTheme()\n const { bottom } = useSafeAreaInsets()\n\n return StyleSheet.create({\n baseBanner: {\n paddingHorizontal: 16,\n paddingVertical: 4,\n backgroundColor: colors.statusNeutralComposedBackground,\n borderTopWidth: 1,\n borderTopColor: colors.borderColorDefaultBase,\n },\n text: {\n textAlign: 'center',\n },\n memberBanner: {\n paddingTop: 16,\n paddingBottom: 16 + bottom,\n marginBottom: -bottom,\n },\n })\n}\n"]}
@@ -1,2 +0,0 @@
1
- export declare const REPLIES_FEATURE_ENABLED = false;
2
- //# sourceMappingURL=replies_local_feature_flag.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"replies_local_feature_flag.d.ts","sourceRoot":"","sources":["../../src/utils/replies_local_feature_flag.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,uBAAuB,QAAQ,CAAA"}
@@ -1,3 +0,0 @@
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
@@ -1 +0,0 @@
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"]}
@@ -1,58 +0,0 @@
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
-
6
- export const LeaderDisabledRepliesBanner = () => {
7
- return <DisabledRepliesBanner description="Replies are frozen for everyone else." />
8
- }
9
-
10
- export const MemberDisabledRepliesBanner = () => {
11
- const styles = useStyles()
12
- return (
13
- <DisabledRepliesBanner
14
- description="Replies have been disabled by a leader, but you can still add reactions."
15
- style={styles.memberBanner}
16
- />
17
- )
18
- }
19
-
20
- interface DisabledRepliesBannerProps {
21
- description: string
22
- style?: ViewStyle
23
- }
24
-
25
- const DisabledRepliesBanner = ({ description, style }: DisabledRepliesBannerProps) => {
26
- const styles = useStyles()
27
-
28
- return (
29
- <View style={[styles.baseBanner, style]}>
30
- <Text style={styles.text} variant="tertiary">
31
- {description}
32
- </Text>
33
- </View>
34
- )
35
- }
36
-
37
- const useStyles = () => {
38
- const { colors } = useTheme()
39
- const { bottom } = useSafeAreaInsets()
40
-
41
- return StyleSheet.create({
42
- baseBanner: {
43
- paddingHorizontal: 16,
44
- paddingVertical: 4,
45
- backgroundColor: colors.statusNeutralComposedBackground,
46
- borderTopWidth: 1,
47
- borderTopColor: colors.borderColorDefaultBase,
48
- },
49
- text: {
50
- textAlign: 'center',
51
- },
52
- memberBanner: {
53
- paddingTop: 16,
54
- paddingBottom: 16 + bottom,
55
- marginBottom: -bottom,
56
- },
57
- })
58
- }
@@ -1,2 +0,0 @@
1
- // TODO: Replace this whole file with the flipper flag
2
- export const REPLIES_FEATURE_ENABLED = false