@planningcenter/chat-react-native 3.11.0-rc.8 → 3.11.0

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 (202) hide show
  1. package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
  2. package/build/components/conversation/attachments/image_attachment.js +9 -2
  3. package/build/components/conversation/attachments/image_attachment.js.map +1 -1
  4. package/build/components/conversation/message.d.ts +1 -1
  5. package/build/components/conversation/message.d.ts.map +1 -1
  6. package/build/components/conversation/message.js +85 -26
  7. package/build/components/conversation/message.js.map +1 -1
  8. package/build/components/conversation/message_form/message_form_attachment_image.d.ts +1 -1
  9. package/build/components/conversation/message_form/message_form_attachment_image.d.ts.map +1 -1
  10. package/build/components/conversation/message_form/message_form_attachment_image.js.map +1 -1
  11. package/build/components/conversation/message_form/message_form_attachment_video.d.ts +1 -1
  12. package/build/components/conversation/message_form/message_form_attachment_video.d.ts.map +1 -1
  13. package/build/components/conversation/message_form/message_form_attachment_video.js.map +1 -1
  14. package/build/components/conversation/message_form.d.ts.map +1 -1
  15. package/build/components/conversation/message_form.js +12 -10
  16. package/build/components/conversation/message_form.js.map +1 -1
  17. package/build/components/conversations/conversations.d.ts.map +1 -1
  18. package/build/components/conversations/conversations.js +5 -1
  19. package/build/components/conversations/conversations.js.map +1 -1
  20. package/build/components/display/spinner.d.ts +6 -1
  21. package/build/components/display/spinner.d.ts.map +1 -1
  22. package/build/components/display/spinner.js +2 -2
  23. package/build/components/display/spinner.js.map +1 -1
  24. package/build/components/display/text.js +10 -2
  25. package/build/components/display/text.js.map +1 -1
  26. package/build/components/primitive/form_sheet.d.ts +1 -0
  27. package/build/components/primitive/form_sheet.d.ts.map +1 -1
  28. package/build/components/primitive/form_sheet.js +2 -2
  29. package/build/components/primitive/form_sheet.js.map +1 -1
  30. package/build/contexts/api_provider.d.ts +1 -0
  31. package/build/contexts/api_provider.d.ts.map +1 -1
  32. package/build/contexts/api_provider.js +24 -2
  33. package/build/contexts/api_provider.js.map +1 -1
  34. package/build/hooks/groups/use_group_members_for_new_conversation.d.ts +1 -1
  35. package/build/hooks/groups/use_groups_conversation_create.d.ts.map +1 -1
  36. package/build/hooks/groups/use_groups_conversation_create.js +3 -1
  37. package/build/hooks/groups/use_groups_conversation_create.js.map +1 -1
  38. package/build/hooks/services/use_find_or_create_services_conversation.d.ts +2 -0
  39. package/build/hooks/services/use_find_or_create_services_conversation.d.ts.map +1 -1
  40. package/build/hooks/services/use_find_or_create_services_conversation.js +22 -19
  41. package/build/hooks/services/use_find_or_create_services_conversation.js.map +1 -1
  42. package/build/hooks/{use_services_team.d.ts → services/use_services_team.d.ts} +1 -1
  43. package/build/hooks/services/use_services_team.d.ts.map +1 -0
  44. package/build/hooks/{use_services_team.js → services/use_services_team.js} +1 -1
  45. package/build/hooks/services/use_services_team.js.map +1 -0
  46. package/build/hooks/use_attachment_uploader.d.ts +5 -13
  47. package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
  48. package/build/hooks/use_attachment_uploader.js.map +1 -1
  49. package/build/hooks/use_chat_permissions.d.ts +10 -0
  50. package/build/hooks/use_chat_permissions.d.ts.map +1 -1
  51. package/build/hooks/use_chat_permissions.js +10 -9
  52. package/build/hooks/use_chat_permissions.js.map +1 -1
  53. package/build/hooks/use_conversation.d.ts +1 -1
  54. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  55. package/build/hooks/use_conversation_messages_jolt_events.js +16 -1
  56. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  57. package/build/hooks/use_giphy.d.ts +1 -1
  58. package/build/hooks/use_giphy.d.ts.map +1 -1
  59. package/build/hooks/use_giphy.js.map +1 -1
  60. package/build/hooks/use_message_create_or_update.d.ts +8 -4
  61. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  62. package/build/hooks/use_message_create_or_update.js +58 -4
  63. package/build/hooks/use_message_create_or_update.js.map +1 -1
  64. package/build/hooks/use_read_receipts.d.ts +1 -1
  65. package/build/hooks/use_suspense_api.d.ts +2 -2
  66. package/build/index.d.ts +1 -1
  67. package/build/index.d.ts.map +1 -1
  68. package/build/index.js +1 -1
  69. package/build/index.js.map +1 -1
  70. package/build/navigation/index.d.ts +11 -0
  71. package/build/navigation/index.d.ts.map +1 -1
  72. package/build/navigation/index.js +10 -0
  73. package/build/navigation/index.js.map +1 -1
  74. package/build/screens/conversation_details_screen.js +1 -1
  75. package/build/screens/conversation_details_screen.js.map +1 -1
  76. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.d.ts.map +1 -1
  77. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js +81 -17
  78. package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js.map +1 -1
  79. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts +171 -4
  80. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.d.ts.map +1 -1
  81. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js +49 -8
  82. package/build/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.js.map +1 -1
  83. package/build/screens/conversation_filter_recipients/types.d.ts +7 -0
  84. package/build/screens/conversation_filter_recipients/types.d.ts.map +1 -1
  85. package/build/screens/conversation_filter_recipients/types.js +6 -0
  86. package/build/screens/conversation_filter_recipients/types.js.map +1 -1
  87. package/build/screens/conversation_filters/components/rows.js +1 -1
  88. package/build/screens/conversation_filters/components/rows.js.map +1 -1
  89. package/build/screens/conversation_new/components/groups_form.js +2 -2
  90. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  91. package/build/screens/conversation_new/components/services_form.d.ts +3 -1
  92. package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
  93. package/build/screens/conversation_new/components/services_form.js +6 -5
  94. package/build/screens/conversation_new/components/services_form.js.map +1 -1
  95. package/build/screens/conversation_new/conversation_new_screen.d.ts +2 -0
  96. package/build/screens/conversation_new/conversation_new_screen.d.ts.map +1 -1
  97. package/build/screens/conversation_new/conversation_new_screen.js +2 -2
  98. package/build/screens/conversation_new/conversation_new_screen.js.map +1 -1
  99. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.d.ts.map +1 -1
  100. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js +38 -33
  101. package/build/screens/conversation_select_recipients/conversation_select_recipients_screen.js.map +1 -1
  102. package/build/screens/team_conversation_screen.d.ts +8 -0
  103. package/build/screens/team_conversation_screen.d.ts.map +1 -0
  104. package/build/screens/team_conversation_screen.js +28 -0
  105. package/build/screens/team_conversation_screen.js.map +1 -0
  106. package/build/types/resources/denormalized_attachment_resource.d.ts +9 -32
  107. package/build/types/resources/denormalized_attachment_resource.d.ts.map +1 -1
  108. package/build/types/resources/denormalized_attachment_resource.js.map +1 -1
  109. package/build/types/resources/denormalized_attachment_resource_for_create.d.ts +50 -0
  110. package/build/types/resources/denormalized_attachment_resource_for_create.d.ts.map +1 -0
  111. package/build/types/resources/denormalized_attachment_resource_for_create.js +2 -0
  112. package/build/types/resources/denormalized_attachment_resource_for_create.js.map +1 -0
  113. package/build/types/resources/message.d.ts +4 -0
  114. package/build/types/resources/message.d.ts.map +1 -1
  115. package/build/types/resources/message.js.map +1 -1
  116. package/build/types/resources/services/chat_resource.d.ts +52 -0
  117. package/build/types/resources/services/chat_resource.d.ts.map +1 -0
  118. package/build/types/resources/services/chat_resource.js +7 -0
  119. package/build/types/resources/services/chat_resource.js.map +1 -0
  120. package/build/types/resources/services/index.d.ts +1 -0
  121. package/build/types/resources/services/index.d.ts.map +1 -1
  122. package/build/types/resources/services/index.js +1 -0
  123. package/build/types/resources/services/index.js.map +1 -1
  124. package/build/types/resources/services/team_resource.d.ts +9 -41
  125. package/build/types/resources/services/team_resource.d.ts.map +1 -1
  126. package/build/types/resources/services/team_resource.js +0 -5
  127. package/build/types/resources/services/team_resource.js.map +1 -1
  128. package/build/utils/cache/optimistically_create_message.d.ts +10 -0
  129. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -0
  130. package/build/utils/cache/optimistically_create_message.js +43 -0
  131. package/build/utils/cache/optimistically_create_message.js.map +1 -0
  132. package/build/utils/cache/optimistically_update_message.d.ts +7 -0
  133. package/build/utils/cache/optimistically_update_message.d.ts.map +1 -0
  134. package/build/utils/cache/optimistically_update_message.js +21 -0
  135. package/build/utils/cache/optimistically_update_message.js.map +1 -0
  136. package/build/utils/cache/page_mutations.d.ts +6 -3
  137. package/build/utils/cache/page_mutations.d.ts.map +1 -1
  138. package/build/utils/cache/page_mutations.js +4 -4
  139. package/build/utils/cache/page_mutations.js.map +1 -1
  140. package/build/utils/convert_attachments_for_create.d.ts +12 -0
  141. package/build/utils/convert_attachments_for_create.d.ts.map +1 -0
  142. package/build/utils/convert_attachments_for_create.js +70 -0
  143. package/build/utils/convert_attachments_for_create.js.map +1 -0
  144. package/build/utils/generate_placeholder_ulid.d.ts +10 -0
  145. package/build/utils/generate_placeholder_ulid.d.ts.map +1 -0
  146. package/build/utils/generate_placeholder_ulid.js +28 -0
  147. package/build/utils/generate_placeholder_ulid.js.map +1 -0
  148. package/build/utils/index.d.ts +1 -0
  149. package/build/utils/index.d.ts.map +1 -1
  150. package/build/utils/index.js +1 -0
  151. package/build/utils/index.js.map +1 -1
  152. package/build/utils/response_error.d.ts +1 -0
  153. package/build/utils/response_error.d.ts.map +1 -1
  154. package/build/utils/response_error.js +6 -0
  155. package/build/utils/response_error.js.map +1 -1
  156. package/package.json +2 -2
  157. package/src/components/conversation/attachments/image_attachment.tsx +25 -10
  158. package/src/components/conversation/message.tsx +116 -28
  159. package/src/components/conversation/message_form/message_form_attachment_image.tsx +1 -1
  160. package/src/components/conversation/message_form/message_form_attachment_video.tsx +1 -1
  161. package/src/components/conversation/message_form.tsx +16 -13
  162. package/src/components/conversations/conversations.tsx +8 -1
  163. package/src/components/display/spinner.tsx +7 -2
  164. package/src/components/display/text.tsx +10 -2
  165. package/src/components/primitive/form_sheet.tsx +3 -2
  166. package/src/contexts/api_provider.tsx +37 -3
  167. package/src/hooks/groups/use_groups_conversation_create.ts +3 -1
  168. package/src/hooks/services/use_find_or_create_services_conversation.ts +29 -21
  169. package/src/hooks/{use_services_team.ts → services/use_services_team.ts} +2 -2
  170. package/src/hooks/use_attachment_uploader.ts +9 -25
  171. package/src/hooks/use_chat_permissions.ts +12 -9
  172. package/src/hooks/use_conversation_messages_jolt_events.ts +19 -1
  173. package/src/hooks/use_giphy.ts +1 -1
  174. package/src/hooks/use_message_create_or_update.ts +82 -6
  175. package/src/index.tsx +1 -1
  176. package/src/navigation/index.tsx +10 -0
  177. package/src/screens/conversation_details_screen.tsx +1 -1
  178. package/src/screens/conversation_filter_recipients/conversation_filter_recipients_screen.tsx +118 -17
  179. package/src/screens/conversation_filter_recipients/hooks/use_service_types_with_teams.ts +61 -10
  180. package/src/screens/conversation_filter_recipients/types.tsx +8 -0
  181. package/src/screens/conversation_filters/components/rows.tsx +1 -1
  182. package/src/screens/conversation_new/components/groups_form.tsx +2 -2
  183. package/src/screens/conversation_new/components/services_form.tsx +17 -3
  184. package/src/screens/conversation_new/conversation_new_screen.tsx +11 -2
  185. package/src/screens/conversation_select_recipients/conversation_select_recipients_screen.tsx +90 -74
  186. package/src/screens/team_conversation_screen.tsx +46 -0
  187. package/src/types/resources/denormalized_attachment_resource.ts +9 -37
  188. package/src/types/resources/denormalized_attachment_resource_for_create.ts +65 -0
  189. package/src/types/resources/message.ts +6 -0
  190. package/src/types/resources/services/chat_resource.ts +66 -0
  191. package/src/types/resources/services/index.ts +1 -0
  192. package/src/types/resources/services/team_resource.ts +10 -53
  193. package/src/utils/__tests__/convert_attachments_for_create.test.ts +175 -0
  194. package/src/utils/cache/optimistically_create_message.ts +71 -0
  195. package/src/utils/cache/optimistically_update_message.ts +37 -0
  196. package/src/utils/cache/page_mutations.ts +5 -3
  197. package/src/utils/convert_attachments_for_create.ts +92 -0
  198. package/src/utils/generate_placeholder_ulid.ts +32 -0
  199. package/src/utils/index.ts +1 -0
  200. package/src/utils/response_error.ts +8 -0
  201. package/build/hooks/use_services_team.d.ts.map +0 -1
  202. package/build/hooks/use_services_team.js.map +0 -1
@@ -1,9 +1,16 @@
1
- import { QueryClient, QueryClientProvider, QueryKey } from '@tanstack/react-query'
1
+ import {
2
+ focusManager,
3
+ QueryClient,
4
+ QueryClientProvider,
5
+ QueryKey,
6
+ usePrefetchQuery,
7
+ } from '@tanstack/react-query'
2
8
  import React, { useContext, useEffect, useRef } from 'react'
3
9
  import { ViewProps } from 'react-native'
4
10
  import { ChatContext, ChatContextValue } from './chat_context'
5
- import { RequestQueryKey } from '../hooks'
11
+ import { appGrantsRequestArgs, getRequestQueryKey, RequestQueryKey } from '../hooks'
6
12
  import { ApiClient, useApiClient } from '../hooks/use_api_client'
13
+ import { useAppState } from '../hooks/use_app_state'
7
14
 
8
15
  let apiClient: ApiClient | undefined
9
16
 
@@ -38,7 +45,34 @@ export function ApiProvider({ children }: ViewProps) {
38
45
  chatQueryClient.clear()
39
46
  }, [sessionChanged])
40
47
 
41
- return <QueryClientProvider client={chatQueryClient}>{children}</QueryClientProvider>
48
+ return (
49
+ <QueryClientProvider client={chatQueryClient}>
50
+ <PrefetchQueries />
51
+ {children}
52
+ </QueryClientProvider>
53
+ )
54
+ }
55
+
56
+ // Component to prefetch queries when the app is focused
57
+ // This needs to live in the provider so that it can access the api client
58
+ // and the chat query client
59
+ const PrefetchQueries = () => {
60
+ usePrefetchQuery({
61
+ queryKey: getRequestQueryKey(appGrantsRequestArgs),
62
+ queryFn: defaultQueryFn,
63
+ })
64
+
65
+ return null
66
+ }
67
+
68
+ export function useFocusManager() {
69
+ const appState = useAppState()
70
+
71
+ useEffect(() => {
72
+ focusManager.setFocused(appState === 'active')
73
+ }, [appState])
74
+
75
+ return appState
42
76
  }
43
77
 
44
78
  function useSessionChanged(value: Pick<ChatContextValue, 'token' | 'env'>): boolean {
@@ -1,6 +1,7 @@
1
1
  import { useMutation } from '@tanstack/react-query'
2
2
  import { ApiResource, ConversationResource, ResourceObject } from '../../types'
3
3
  import { useApiClient } from '../use_api_client'
4
+ import { throwResponseError } from '../../utils/response_error'
4
5
 
5
6
  interface Props {
6
7
  groupId: number
@@ -41,7 +42,8 @@ export function useGroupsConversationCreate({ groupId, title, onSuccess }: Props
41
42
  },
42
43
  },
43
44
  })
44
- ),
45
+ )
46
+ .catch(throwResponseError),
45
47
  })
46
48
  }
47
49
 
@@ -7,6 +7,7 @@ import {
7
7
  ServicesChatPayloadResource,
8
8
  } from '../../types'
9
9
  import { ApiClient, useApiClient } from '../use_api_client'
10
+ import { throwResponseError } from '../../utils/response_error'
10
11
 
11
12
  interface Props {
12
13
  teamIds: number[]
@@ -15,34 +16,41 @@ interface Props {
15
16
  }
16
17
 
17
18
  export function useFindOrCreateServicesConversation({ teamIds, planId, onSuccess }: Props) {
18
- const teamAndPlanParams: TeamAndPlanParams = {
19
- team_id: teamIds.join(','),
20
- ...(planId ? { plan_id: planId } : {}),
21
- }
22
-
23
19
  const apiClient = useApiClient()
24
20
  return useMutation({
25
21
  throwOnError: true,
26
22
  onSuccess: result => {
27
23
  onSuccess && onSuccess(result)
28
24
  },
29
- mutationFn: async () => {
30
- const foundConversations = await getGroupIdsFromServices(apiClient, teamAndPlanParams)
31
- .then(res => res.data.groupIdentifiers)
32
- .then(groupIdentifiers => findConversationWithExactTeams(apiClient, groupIdentifiers))
33
- .catch(() => null)
34
- const foundConversation = foundConversations?.data[0]
25
+ mutationFn: async () => findOrCreateServicesConversation(apiClient, teamIds, planId),
26
+ })
27
+ }
35
28
 
36
- if (foundConversation?.id) {
37
- return foundConversation
38
- }
29
+ export const findOrCreateServicesConversation = async (
30
+ apiClient: ApiClient,
31
+ teamIds: number[],
32
+ planId?: number
33
+ ) => {
34
+ const teamAndPlanParams: TeamAndPlanParams = {
35
+ team_id: teamIds.join(','),
36
+ ...(planId ? { plan_id: planId } : {}),
37
+ }
39
38
 
40
- return fetchServicesPayload(apiClient, teamAndPlanParams)
41
- .then(res => res.data.payload)
42
- .then(payload => createConversation(apiClient, payload))
43
- .then(res => res.data)
44
- },
45
- })
39
+ const foundConversations = await getGroupIdsFromServices(apiClient, teamAndPlanParams)
40
+ .then(res => res.data.groupIdentifiers)
41
+ .then(groupIdentifiers => findConversationWithExactTeams(apiClient, groupIdentifiers))
42
+ .catch(() => null)
43
+ const foundConversation = foundConversations?.data[0]
44
+
45
+ if (foundConversation?.id) {
46
+ return foundConversation
47
+ }
48
+
49
+ return fetchServicesPayload(apiClient, teamAndPlanParams)
50
+ .then(res => res.data.payload)
51
+ .then(payload => createConversation(apiClient, payload))
52
+ .then(res => res.data)
53
+ .catch(throwResponseError)
46
54
  }
47
55
 
48
56
  interface TeamAndPlanParams {
@@ -79,7 +87,7 @@ function findConversationWithExactTeams(apiClient: ApiClient, groupIdentifiers:
79
87
  url: '/me/conversations',
80
88
  data: {
81
89
  fields: {
82
- Conversation: ['stream_channel'],
90
+ Conversation: ['stream_channel', 'title'],
83
91
  },
84
92
  filter: 'with_exact_groups',
85
93
  gids: groupIdentifiers.join(','),
@@ -1,5 +1,5 @@
1
- import { TeamOptionResource, TeamOptionResponseItem } from '../types'
2
- import { useApiGet } from './use_api'
1
+ import { TeamOptionResource, TeamOptionResponseItem } from '../../types'
2
+ import { useApiGet } from '../use_api'
3
3
 
4
4
  export const useServicesTeams = () => {
5
5
  const { data: chat } = useApiGet<TeamOptionResource>({
@@ -1,31 +1,15 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { SUPPORTED_EXTENSIONS } from './attachments/supported_extensions'
3
- import { FileForUploadClient, useUploadClient } from './use_upload_client'
3
+ import { useUploadClient } from './use_upload_client'
4
4
  import { useApiClient } from './use_api_client'
5
5
  import { ApiResource } from '../types'
6
+ import {
7
+ FileAttachment,
8
+ FileUploadState,
9
+ NativeAttachmentFile,
10
+ } from '../types/resources/denormalized_attachment_resource_for_create'
6
11
 
7
- type AttachmentStatus = 'uploading' | 'success' | 'error'
8
-
9
- interface AttachmentFile extends FileForUploadClient {
10
- size: number
11
- width?: number
12
- height?: number
13
- }
14
-
15
- export interface FileAttachment {
16
- id?: string
17
- file: AttachmentFile
18
- status: AttachmentStatus
19
- }
20
-
21
- interface UploadState {
22
- [fileName: string]: {
23
- status: AttachmentStatus
24
- id?: string
25
- }
26
- }
27
-
28
- interface FileError {
12
+ export interface FileError {
29
13
  file_type?: string[]
30
14
  file_size?: boolean
31
15
  }
@@ -38,13 +22,13 @@ export function useAttachmentUploader({ conversationId }: { conversationId: numb
38
22
  const apiClient = useApiClient()
39
23
  const uploadApi = useUploadClient()
40
24
  const [attachments, setAttachments] = useState<FileAttachment[]>([])
41
- const uploadState = useRef<UploadState>({})
25
+ const uploadState = useRef<FileUploadState>({})
42
26
  const [lastUploadId, setLastUploadId] = useState<string>()
43
27
  const numberOfAttachments = attachments.length
44
28
  const [errorMessage, setErrorMessage] = useState<string | null>(null)
45
29
 
46
30
  const handleFilesAttached = useCallback(
47
- (files: AttachmentFile[]) => {
31
+ (files: NativeAttachmentFile[]) => {
48
32
  const fileErrors = {} as FileError
49
33
 
50
34
  const validFiles = files.filter(file => {
@@ -1,16 +1,19 @@
1
1
  import { AppGrantsResource } from '../types'
2
2
  import { useApiGet } from './use_api'
3
+ import { App } from './use_api_client'
3
4
 
4
- export function useAppGrants() {
5
- return useApiGet<AppGrantsResource[]>({
6
- url: '/me/app_grants',
7
- data: {
8
- fields: {
9
- AppGrant: ['create_conversations', 'app_name'],
10
- },
5
+ export const appGrantsRequestArgs = {
6
+ url: '/me/app_grants',
7
+ data: {
8
+ fields: {
9
+ AppGrant: ['create_conversations', 'app_name'],
11
10
  },
12
- app: 'chat',
13
- })
11
+ },
12
+ app: 'chat' as App,
13
+ }
14
+
15
+ export function useAppGrants() {
16
+ return useApiGet<AppGrantsResource[]>(appGrantsRequestArgs)
14
17
  }
15
18
 
16
19
  export function useCanCreateConversations(): boolean {
@@ -14,6 +14,7 @@ import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'
14
14
  import { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'
15
15
  import { getMessagesRequestArgs } from '../utils/request/messages'
16
16
  import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
17
+ import { isTemporaryMessageId } from './use_message_create_or_update'
17
18
 
18
19
  interface Props {
19
20
  conversationId: number
@@ -42,8 +43,25 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
42
43
 
43
44
  queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {
44
45
  if (e.event === 'message.created') {
46
+ // Before adding the new message, remove any pending temporary messages
47
+ // with matching text to prevent duplicates from race conditions
48
+ let dataAfterTempRemoval = prev
49
+ if (prev && message.text && message.mine) {
50
+ dataAfterTempRemoval = deleteRecordInPagesData({
51
+ data: prev,
52
+ record: message,
53
+ matchFn: (existingMessage, _record) => {
54
+ return (
55
+ isTemporaryMessageId(existingMessage.id) &&
56
+ existingMessage.text === message.text &&
57
+ existingMessage.mine
58
+ )
59
+ },
60
+ })
61
+ }
62
+
45
63
  return updateOrCreateRecordInPagesData({
46
- data: prev,
64
+ data: dataAfterTempRemoval,
47
65
  record: message,
48
66
  processRecord: (record, current) => {
49
67
  return { ...current, ...record }
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useContext, useEffect, useState } from 'react'
2
- import { DenormalizedGiphyAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource'
2
+ import { DenormalizedGiphyAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'
3
3
  import { ChatContext } from '../contexts/chat_context'
4
4
 
5
5
  interface GiphyGif {
@@ -3,17 +3,27 @@ import { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_
3
3
  import { useApiClient } from './use_api_client'
4
4
  import { ApiCollection, ApiResource, MessageResource } from '../types'
5
5
  import { chatQueryClient } from '../contexts/api_provider'
6
- import { updateOrCreateRecordInPagesData } from '../utils'
7
- import { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource'
6
+ import {
7
+ deleteRecordInPagesData,
8
+ updateOrCreateRecordInPagesData,
9
+ updateRecordInPagesData,
10
+ } from '../utils'
11
+ import { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'
12
+ import { useCurrentPerson } from './use_current_person'
13
+ import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'
14
+ import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'
8
15
 
9
16
  interface Props {
10
17
  conversationId: number
11
- messageId?: string | number | null
18
+ message?: MessageResource
12
19
  }
13
20
 
14
- export function useMessageCreateOrUpdate({ conversationId, messageId }: Props) {
15
- const isEditing = !!messageId
21
+ export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
22
+ const messageId = message?.id || null
23
+ const isEditing = !isNewMessage(message)
16
24
  const apiClient = useApiClient()
25
+ const currentPerson = useCurrentPerson()
26
+
17
27
  const mutation = useMutation({
18
28
  mutationFn: ({
19
29
  text,
@@ -47,11 +57,70 @@ export function useMessageCreateOrUpdate({ conversationId, messageId }: Props) {
47
57
  })
48
58
  }
49
59
  },
50
- onSuccess: (result: ApiResource<MessageResource>) => {
60
+ onMutate: async ({
61
+ text,
62
+ attachments,
63
+ }: {
64
+ text: string
65
+ attachments?: DenormalizedAttachmentResourceForCreate[]
66
+ }) => {
67
+ if (message && isEditing) {
68
+ const optimisticMessage = optimisticallyUpdateMessage({
69
+ conversationId,
70
+ message,
71
+ text,
72
+ })
73
+
74
+ return { message: optimisticMessage }
75
+ }
76
+
77
+ const optimisticMessage = optimisticallyCreateMessage({
78
+ conversationId,
79
+ message,
80
+ text,
81
+ attachments,
82
+ currentPerson,
83
+ })
84
+
85
+ return { message: optimisticMessage }
86
+ },
87
+ onError: (error, variables, context) => {
88
+ const { message: optimisticMessage } = context || {}
89
+ // Add error to the optimistic message from the cache on error
90
+ if (optimisticMessage) {
91
+ const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
92
+ chatQueryClient.setQueryData(
93
+ queryKey,
94
+ (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>
95
+ updateRecordInPagesData({
96
+ data,
97
+ record: optimisticMessage,
98
+ processRecord: (_next, prev) => ({
99
+ ...prev,
100
+ error: error.message || 'Failed to send message',
101
+ pending: false, // Mark as no longer pending
102
+ }),
103
+ })
104
+ )
105
+ }
106
+ },
107
+ onSuccess: (result: ApiResource<MessageResource>, variables, context) => {
108
+ const { message: optimisticMessage } = context || {}
51
109
  const updatedMessage = result.data
52
110
  type QueryData = InfiniteData<ApiCollection<MessageResource>>
53
111
  const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
54
112
 
113
+ // First remove the optimistic message if it exists
114
+ if (optimisticMessage) {
115
+ chatQueryClient.setQueryData<QueryData>(queryKey, data =>
116
+ deleteRecordInPagesData({
117
+ data,
118
+ record: optimisticMessage,
119
+ })
120
+ )
121
+ }
122
+
123
+ // Then add the real message
55
124
  chatQueryClient.setQueryData<QueryData>(queryKey, data =>
56
125
  updateOrCreateRecordInPagesData({
57
126
  data,
@@ -63,3 +132,10 @@ export function useMessageCreateOrUpdate({ conversationId, messageId }: Props) {
63
132
 
64
133
  return mutation
65
134
  }
135
+
136
+ export function isTemporaryMessageId(messageId?: string | null): boolean {
137
+ return !!messageId && messageId.endsWith('-temp')
138
+ }
139
+ export function isNewMessage(message?: MessageResource): boolean {
140
+ return !message?.id || isTemporaryMessageId(message.id)
141
+ }
package/src/index.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  export { GroupConversations } from './components'
2
- export { ApiProvider, chatQueryClient } from './contexts/api_provider'
2
+ export { ApiProvider, chatQueryClient, useFocusManager } from './contexts/api_provider'
3
3
  export * from './contexts/chat_context'
4
4
  export * from './navigation'
5
5
  export { ScreenLayout } from './navigation/screenLayout'
@@ -43,6 +43,8 @@ import {
43
43
  } from '../screens/conversation/message_read_receipts_screen'
44
44
  import { Platform } from 'react-native'
45
45
  import { HeaderSubmitButton } from '../components/display/platform_modal_header_buttons'
46
+ import { TeamConversationScreen } from '../screens/team_conversation_screen'
47
+ import { CardStyleInterpolators } from '@react-navigation/stack'
46
48
 
47
49
  const HEADER_BACK_BUTTON_LAYOUT_RESET_STYLES = {
48
50
  marginLeft: Platform.select({ ios: -8, default: -3 }),
@@ -179,6 +181,14 @@ export const ChatStack = createNativeStackNavigator({
179
181
  ),
180
182
  }),
181
183
  },
184
+ TeamConversation: {
185
+ screen: TeamConversationScreen,
186
+ options: {
187
+ title: 'Finding conversation...',
188
+ animation: 'none',
189
+ cardStyleInterpolator: CardStyleInterpolators.forNoAnimation,
190
+ },
191
+ },
182
192
  ConversationDetails: {
183
193
  screen: ConversationDetailsScreen,
184
194
  options: ({ navigation }) => ({
@@ -242,7 +242,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
242
242
  {
243
243
  type: canUpdate ? SectionTypes.setting : SectionTypes.hidden,
244
244
  data: {
245
- title: 'Freeze converation',
245
+ title: 'Freeze conversation',
246
246
  subtitle: 'Disables replies for everyone except leaders.',
247
247
  rightItem: (
248
248
  <Switch value={repliesDisabled} onChange={() => setRepliesDisabled(!repliesDisabled)} />
@@ -1,10 +1,10 @@
1
- import { StackActions, useNavigation } from '@react-navigation/native'
1
+ import { RouteProp, StackActions, useNavigation, useRoute } from '@react-navigation/native'
2
2
  import { NativeStackNavigationProp } from '@react-navigation/native-stack'
3
- import React, { useCallback } from 'react'
4
- import { Platform, StyleSheet } from 'react-native'
5
- import { FlatList } from 'react-native-gesture-handler'
3
+ import React, { useCallback, useMemo, useState } from 'react'
4
+ import { LayoutChangeEvent, Platform, StyleSheet, View } from 'react-native'
5
+ import { FlatList, TextInput } from 'react-native-gesture-handler'
6
6
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
7
- import { Heading } from '../../components'
7
+ import { BlankState, Heading, ToggleButton } from '../../components'
8
8
  import FormSheet, { getFormSheetScreenOptions } from '../../components/primitive/form_sheet'
9
9
  import { useTheme } from '../../hooks'
10
10
  import { tokens } from '../../vendor/tapestry/tokens'
@@ -12,7 +12,8 @@ import { CheckboxRow } from './components/checkbox_row'
12
12
  import { HeaderRow } from './components/header_row'
13
13
  import { useFlattenedArrayOfServiceTypesWithTeams } from './hooks/use_flattened_array_of_service_types_with_teams'
14
14
  import { useServiceTypesWithTeams } from './hooks/use_service_types_with_teams'
15
- import { ConversationFilterRecipientsScreenProps, SectionTypes } from './types'
15
+ import { ConversationFilterRecipientsScreenProps, SectionTypes, TeamFilterTypes } from './types'
16
+ import { DefaultLoading } from '../../components/page/loading'
16
17
 
17
18
  const SERVICE_TYPE_LABELLED_BY_PREFIX = 'header-'
18
19
 
@@ -28,18 +29,34 @@ export const ConversationFilterRecipientsScreen = ({
28
29
  route,
29
30
  }: ConversationFilterRecipientsScreenProps) => {
30
31
  const styles = useStyles()
32
+
33
+ // Set the height of the container to accommodate the team filters
34
+ const [teamFiltersHeight, setTeamFiltersHeight] = useState(200)
35
+ const { top, bottom } = useSafeAreaInsets()
36
+ const listContainerStyle = useMemo(
37
+ () => Platform.select({ ios: { paddingBottom: teamFiltersHeight + bottom + top } }),
38
+ [teamFiltersHeight, bottom, top]
39
+ )
40
+
31
41
  const navigation =
32
42
  useNavigation<NativeStackNavigationProp<ConversationFilterRecipientsScreenProps>>()
33
43
  const { team_ids: teamIds } = route.params
44
+ const { params } = route
45
+ const teamIdCount = Number(teamIds?.length)
46
+ const headerTitle = useMemo(() => {
47
+ return teamIdCount >= 1 ? `Selected teams (${teamIdCount})` : 'Select teams'
48
+ }, [teamIdCount])
34
49
 
35
- const { serviceTypes } = useServiceTypesWithTeams()
50
+ const { serviceTypes, isFetched } = useServiceTypesWithTeams({
51
+ filterType: params.team_filter_type,
52
+ searchQuery: params.search_query,
53
+ })
36
54
  const data = useFlattenedArrayOfServiceTypesWithTeams({
37
55
  data: serviceTypes,
38
56
  firstRowStyle: styles.firstRow,
39
57
  lastRowStyle: styles.lastRow,
40
58
  })
41
59
 
42
- const { params } = route
43
60
  const noTeamsSelected = params.team_ids?.length === 0 || !params.team_ids
44
61
  const applyButtonAccessibilityHint = noTeamsSelected
45
62
  ? 'Select at least one team to navigate to the final step in creating your conversation.'
@@ -47,9 +64,7 @@ export const ConversationFilterRecipientsScreen = ({
47
64
 
48
65
  const setTeamFilters = useCallback(
49
66
  ({ team_ids }: { team_ids: number[] }) => {
50
- navigation.setParams({
51
- team_ids,
52
- })
67
+ navigation.setParams({ team_ids })
53
68
  },
54
69
  [navigation]
55
70
  )
@@ -62,13 +77,14 @@ export const ConversationFilterRecipientsScreen = ({
62
77
  navigation.dispatch(StackActions.popTo('ConversationNew', params))
63
78
  }, [navigation, params])
64
79
 
65
- const teamIdCount = Number(teamIds?.length)
66
- const count = teamIdCount > 1 ? ` (${teamIdCount})` : ''
80
+ const handleTeamFiltersLayout = useCallback((event: LayoutChangeEvent) => {
81
+ setTeamFiltersHeight(event.nativeEvent.layout.height)
82
+ }, [])
67
83
 
68
84
  return (
69
85
  <FormSheet.Root style={styles.root}>
70
- <FormSheet.Header>
71
- <FormSheet.HeaderTitle>Teams I lead{count}</FormSheet.HeaderTitle>
86
+ <FormSheet.Header style={styles.header}>
87
+ <FormSheet.HeaderTitle>{headerTitle}</FormSheet.HeaderTitle>
72
88
  <FormSheet.HeaderActions>
73
89
  <FormSheet.HeaderSecondaryButton
74
90
  onPress={resetTeamFilters}
@@ -84,7 +100,7 @@ export const ConversationFilterRecipientsScreen = ({
84
100
  />
85
101
  </FormSheet.HeaderActions>
86
102
  </FormSheet.Header>
87
-
103
+ <TeamFilters onLayout={handleTeamFiltersLayout} />
88
104
  <FlatList
89
105
  data={data}
90
106
  ListHeaderComponent={
@@ -92,10 +108,11 @@ export const ConversationFilterRecipientsScreen = ({
92
108
  Service Types
93
109
  </Heading>
94
110
  }
95
- contentContainerStyle={styles.listContentContainer}
111
+ contentContainerStyle={[styles.listContentContainer, listContainerStyle]}
96
112
  keyExtractor={item =>
97
113
  `${item.type === SectionTypes.header ? item.data.serviceTypeId : item.data.teamId}`
98
114
  }
115
+ ListEmptyComponent={isFetched ? <BlankState title="No teams found" /> : <DefaultLoading />}
99
116
  renderItem={({ item }) => {
100
117
  switch (item.type) {
101
118
  case SectionTypes.header:
@@ -125,6 +142,63 @@ export const ConversationFilterRecipientsScreen = ({
125
142
  )
126
143
  }
127
144
 
145
+ const TeamFilters = ({ onLayout }: { onLayout?: (event: LayoutChangeEvent) => void }) => {
146
+ const styles = useStyles()
147
+ const { colors } = useTheme()
148
+ const navigation =
149
+ useNavigation<NativeStackNavigationProp<ConversationFilterRecipientsScreenProps>>()
150
+ const route = useRoute<RouteProp<ConversationFilterRecipientsScreenProps['route']>>()
151
+ const { params } = route
152
+
153
+ const active = params.team_filter_type || TeamFilterTypes.TeamsIlead
154
+
155
+ const handleFilterChange = useCallback(
156
+ (filter: TeamFilterTypes) => {
157
+ navigation.setParams({
158
+ team_filter_type: filter,
159
+ team_ids: [],
160
+ })
161
+ },
162
+ [navigation]
163
+ )
164
+
165
+ return (
166
+ <View style={styles.teamFiltersContainer} onLayout={onLayout}>
167
+ <TextInput
168
+ defaultValue={params.search_query}
169
+ placeholder="Search teams"
170
+ placeholderTextColor={colors.textColorDefaultPlaceholder}
171
+ style={styles.searchInput}
172
+ onChangeText={text => {
173
+ navigation.setParams({
174
+ search_query: text,
175
+ })
176
+ }}
177
+ />
178
+ <View style={styles.filterToggleContainer}>
179
+ <ToggleButton
180
+ active={active === TeamFilterTypes.TeamsIlead}
181
+ title={TeamFilterTypes.TeamsIlead}
182
+ accessibilityLabel="Show all teams I lead"
183
+ onPress={() => handleFilterChange(TeamFilterTypes.TeamsIlead)}
184
+ />
185
+ <ToggleButton
186
+ active={active === TeamFilterTypes.MyTeams}
187
+ title={TeamFilterTypes.MyTeams}
188
+ accessibilityLabel="Show all my teams"
189
+ onPress={() => handleFilterChange(TeamFilterTypes.MyTeams)}
190
+ />
191
+ <ToggleButton
192
+ active={active === TeamFilterTypes.All}
193
+ title={TeamFilterTypes.All}
194
+ accessibilityLabel="Show all teams"
195
+ onPress={() => handleFilterChange(TeamFilterTypes.All)}
196
+ />
197
+ </View>
198
+ </View>
199
+ )
200
+ }
201
+
128
202
  const useStyles = () => {
129
203
  const { colors } = useTheme()
130
204
  const { bottom } = useSafeAreaInsets()
@@ -133,6 +207,9 @@ const useStyles = () => {
133
207
  root: {
134
208
  backgroundColor: colors.surfaceColor080,
135
209
  },
210
+ header: {
211
+ borderBottomWidth: 0,
212
+ },
136
213
  listContentContainer: {
137
214
  padding: 16,
138
215
  paddingBottom: bottom + Platform.select({ android: 24, default: 16 }),
@@ -149,5 +226,29 @@ const useStyles = () => {
149
226
  borderBottomEndRadius: tokens.borderRadiusLg,
150
227
  marginBottom: 16,
151
228
  },
229
+ teamFiltersContainer: {
230
+ gap: 12,
231
+ paddingHorizontal: 16,
232
+ paddingBottom: 28,
233
+ backgroundColor: colors.fillColorNeutral100Inverted,
234
+ borderBottomWidth: 1,
235
+ borderBottomColor: colors.borderColorDefaultBase,
236
+ },
237
+ filterToggleContainer: {
238
+ flexDirection: 'row',
239
+ gap: 8,
240
+ },
241
+ searchInput: {
242
+ paddingVertical: 16,
243
+ paddingHorizontal: 16,
244
+ color: colors.textColorDefaultPrimary,
245
+ textAlignVertical: 'center',
246
+ justifyContent: 'center',
247
+ fontSize: 16,
248
+ borderWidth: 1,
249
+ borderColor: colors.borderColorDefaultBase,
250
+ borderRadius: 24,
251
+ backgroundColor: colors.fillColorNeutral100Inverted,
252
+ },
152
253
  })
153
254
  }