@planningcenter/chat-react-native 3.2.0-rc.25 → 3.2.0-rc.27

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 (102) hide show
  1. package/build/components/conversation/message_form/message_form_attachment_image.d.ts +13 -0
  2. package/build/components/conversation/message_form/message_form_attachment_image.d.ts.map +1 -0
  3. package/build/components/conversation/message_form/message_form_attachment_image.js +78 -0
  4. package/build/components/conversation/message_form/message_form_attachment_image.js.map +1 -0
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +128 -16
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversations/conversation_actions.d.ts +2 -2
  9. package/build/components/conversations/conversation_actions.d.ts.map +1 -1
  10. package/build/components/conversations/conversation_actions.js.map +1 -1
  11. package/build/components/conversations/conversation_preview.d.ts +3 -1
  12. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  13. package/build/components/conversations/conversation_preview.js +2 -2
  14. package/build/components/conversations/conversation_preview.js.map +1 -1
  15. package/build/components/group_conversation_list.d.ts +19 -0
  16. package/build/components/group_conversation_list.d.ts.map +1 -0
  17. package/build/components/group_conversation_list.js +48 -0
  18. package/build/components/group_conversation_list.js.map +1 -0
  19. package/build/components/index.d.ts +1 -0
  20. package/build/components/index.d.ts.map +1 -1
  21. package/build/components/index.js +1 -0
  22. package/build/components/index.js.map +1 -1
  23. package/build/contexts/conversations_context.js +1 -1
  24. package/build/contexts/conversations_context.js.map +1 -1
  25. package/build/hooks/attachments/supported_extensions.d.ts +2 -0
  26. package/build/hooks/attachments/supported_extensions.d.ts.map +1 -0
  27. package/build/hooks/attachments/supported_extensions.js +48 -0
  28. package/build/hooks/attachments/supported_extensions.js.map +1 -0
  29. package/build/hooks/index.d.ts +4 -0
  30. package/build/hooks/index.d.ts.map +1 -1
  31. package/build/hooks/index.js +4 -0
  32. package/build/hooks/index.js.map +1 -1
  33. package/build/hooks/use_api.d.ts +2 -2
  34. package/build/hooks/use_api.d.ts.map +1 -1
  35. package/build/hooks/use_api.js.map +1 -1
  36. package/build/hooks/use_attachment_uploader.d.ts +26 -0
  37. package/build/hooks/use_attachment_uploader.d.ts.map +1 -0
  38. package/build/hooks/use_attachment_uploader.js +111 -0
  39. package/build/hooks/use_attachment_uploader.js.map +1 -0
  40. package/build/hooks/use_upload_client.d.ts +28 -0
  41. package/build/hooks/use_upload_client.d.ts.map +1 -0
  42. package/build/hooks/use_upload_client.js +32 -0
  43. package/build/hooks/use_upload_client.js.map +1 -0
  44. package/build/index.d.ts +1 -0
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +1 -0
  47. package/build/index.js.map +1 -1
  48. package/build/navigation/index.d.ts +2 -2
  49. package/build/navigation/index.d.ts.map +1 -1
  50. package/build/navigation/index.js +2 -2
  51. package/build/navigation/index.js.map +1 -1
  52. package/build/screens/conversation_new/components/groups_form.d.ts +3 -1
  53. package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
  54. package/build/screens/conversation_new/components/groups_form.js +7 -9
  55. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  56. package/build/screens/conversation_new/conversation_new_screen.d.ts.map +1 -1
  57. package/build/screens/conversation_new/conversation_new_screen.js +2 -2
  58. package/build/screens/conversation_new/conversation_new_screen.js.map +1 -1
  59. package/build/screens/conversation_screen.d.ts.map +1 -1
  60. package/build/screens/conversation_screen.js +27 -2
  61. package/build/screens/conversation_screen.js.map +1 -1
  62. package/build/screens/conversations/conversations_screen.js +1 -1
  63. package/build/screens/conversations/conversations_screen.js.map +1 -1
  64. package/build/utils/native_adapters/configuration.d.ts +4 -1
  65. package/build/utils/native_adapters/configuration.d.ts.map +1 -1
  66. package/build/utils/native_adapters/configuration.js +13 -1
  67. package/build/utils/native_adapters/configuration.js.map +1 -1
  68. package/build/utils/native_adapters/image_picker.d.ts +25 -0
  69. package/build/utils/native_adapters/image_picker.d.ts.map +1 -0
  70. package/build/utils/native_adapters/image_picker.js +9 -0
  71. package/build/utils/native_adapters/image_picker.js.map +1 -0
  72. package/build/utils/native_adapters/index.d.ts +1 -0
  73. package/build/utils/native_adapters/index.d.ts.map +1 -1
  74. package/build/utils/native_adapters/index.js +1 -0
  75. package/build/utils/native_adapters/index.js.map +1 -1
  76. package/build/utils/upload_uri.d.ts +23 -0
  77. package/build/utils/upload_uri.d.ts.map +1 -0
  78. package/build/utils/upload_uri.js +60 -0
  79. package/build/utils/upload_uri.js.map +1 -0
  80. package/package.json +2 -2
  81. package/src/components/conversation/message_form/message_form_attachment_image.tsx +121 -0
  82. package/src/components/conversation/message_form.tsx +197 -31
  83. package/src/components/conversations/conversation_actions.tsx +2 -2
  84. package/src/components/conversations/conversation_preview.tsx +8 -2
  85. package/src/components/group_conversation_list.tsx +82 -0
  86. package/src/components/index.tsx +1 -0
  87. package/src/contexts/conversations_context.tsx +1 -1
  88. package/src/hooks/attachments/supported_extensions.ts +47 -0
  89. package/src/hooks/index.ts +4 -0
  90. package/src/hooks/use_api.ts +2 -2
  91. package/src/hooks/use_attachment_uploader.ts +179 -0
  92. package/src/hooks/use_upload_client.ts +67 -0
  93. package/src/index.tsx +1 -0
  94. package/src/navigation/index.tsx +2 -5
  95. package/src/screens/conversation_new/components/groups_form.tsx +11 -11
  96. package/src/screens/conversation_new/conversation_new_screen.tsx +6 -2
  97. package/src/screens/conversation_screen.tsx +31 -1
  98. package/src/screens/conversations/conversations_screen.tsx +1 -1
  99. package/src/utils/native_adapters/configuration.ts +15 -1
  100. package/src/utils/native_adapters/image_picker.ts +31 -0
  101. package/src/utils/native_adapters/index.ts +1 -0
  102. package/src/utils/upload_uri.ts +69 -0
@@ -0,0 +1,47 @@
1
+ export const SUPPORTED_EXTENSIONS = [
2
+ '.3ga',
3
+ '.3gp',
4
+ '.aac',
5
+ '.amr',
6
+ '.avi',
7
+ '.bmp',
8
+ '.doc',
9
+ '.docx',
10
+ '.gif',
11
+ '.h263',
12
+ '.h264',
13
+ '.heic',
14
+ '.heif',
15
+ '.jpeg',
16
+ '.jpg',
17
+ '.key',
18
+ '.m4a',
19
+ '.m4b',
20
+ '.m4p',
21
+ '.m4r',
22
+ '.m4v',
23
+ '.mkv',
24
+ '.mov',
25
+ '.mp3',
26
+ '.mp4',
27
+ '.mp4-latm',
28
+ '.mpeg',
29
+ '.mpeg4',
30
+ '.mpg',
31
+ '.numbers',
32
+ '.ogg',
33
+ '.pages',
34
+ '.pdf',
35
+ '.png',
36
+ '.ppt',
37
+ '.pptx',
38
+ '.rtf',
39
+ '.txt',
40
+ '.vcf',
41
+ '.wav',
42
+ '.webm',
43
+ '.webp',
44
+ '.wmv',
45
+ '.xls',
46
+ '.xlsx',
47
+ ]
@@ -6,3 +6,7 @@ export * from './use_font_scale'
6
6
  export * from './use_create_android_ripple_color'
7
7
  export * from './use_chat_permissions'
8
8
  export * from './use_api_client'
9
+ export * from './use_groups_groups'
10
+ export * from './use_groups'
11
+ export * from './use_api'
12
+ export * from './use_api_client'
@@ -36,14 +36,14 @@ type NextMeta = Partial<{
36
36
  idLt: string
37
37
  }>
38
38
 
39
- export type SuspensePaginatorOptions = Omit<
39
+ export type PaginatorOptions = Omit<
40
40
  AnyUseSuspenseInfiniteQueryOptions,
41
41
  'getNextPageParam' | 'initialPageParam' | 'queryFn' | 'queryKey'
42
42
  >
43
43
 
44
44
  export const useApiPaginator = <T extends ResourceObject>(
45
45
  args: ApiGetOptions,
46
- opts?: SuspensePaginatorOptions
46
+ opts?: PaginatorOptions
47
47
  ) => {
48
48
  const apiClient = useApiClient()
49
49
  const query = useInfiniteQuery<
@@ -0,0 +1,179 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { SUPPORTED_EXTENSIONS } from './attachments/supported_extensions'
3
+ import { FileForUploadClient, useUploadClient } from './use_upload_client'
4
+ import { useApiClient } from './use_api_client'
5
+ import { ApiResource } from '../types'
6
+
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 {
29
+ file_type?: string[]
30
+ file_size?: boolean
31
+ }
32
+
33
+ const MAX_FILE_SIZE_IN_MB = 50
34
+ const MAX_FILE_SIZE_IN_BYTES = MAX_FILE_SIZE_IN_MB * 1024 * 1024
35
+ const MAX_NUMBER_OF_ATTACHMENTS = 10
36
+
37
+ export function useAttachmentUploader({ conversationId }: { conversationId: number }) {
38
+ const apiClient = useApiClient()
39
+ const uploadApi = useUploadClient()
40
+ const [attachments, setAttachments] = useState<FileAttachment[]>([])
41
+ const uploadState = useRef<UploadState>({})
42
+ const [lastUploadId, setLastUploadId] = useState<string>()
43
+ const numberOfAttachments = attachments.length
44
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
45
+
46
+ const handleFilesAttached = useCallback(
47
+ (files: AttachmentFile[]) => {
48
+ const fileErrors = {} as FileError
49
+
50
+ const validFiles = files.filter(file => {
51
+ const extension = file.name.split('.').pop() as string
52
+ const isValidExtension = SUPPORTED_EXTENSIONS.includes(`.${extension}`)
53
+ const isValidFileSize = file.size <= MAX_FILE_SIZE_IN_BYTES
54
+
55
+ if (!isValidExtension) {
56
+ fileErrors.file_type ||= []
57
+ fileErrors.file_type.push(extension)
58
+ }
59
+ if (!isValidFileSize) {
60
+ fileErrors.file_size = true
61
+ }
62
+
63
+ return isValidFileSize && isValidExtension
64
+ })
65
+
66
+ const errorMessages: string[] = []
67
+ if (fileErrors.file_type) {
68
+ errorMessages.push(
69
+ `The following file types are not supported: ${fileErrors.file_type.join(', ')}`
70
+ )
71
+ }
72
+ if (fileErrors.file_size) {
73
+ errorMessages.push(`File size exceeds ${MAX_FILE_SIZE_IN_MB} MB`)
74
+ }
75
+ if (numberOfAttachments + validFiles.length > MAX_NUMBER_OF_ATTACHMENTS) {
76
+ errorMessages.push(`You can't attach more than ${MAX_NUMBER_OF_ATTACHMENTS} files at once.`)
77
+ }
78
+ if (errorMessages.length > 0) {
79
+ setErrorMessage(errorMessages.join('\n'))
80
+ return
81
+ }
82
+
83
+ const newAttachments: FileAttachment[] = validFiles.map(file => ({
84
+ file,
85
+ status: 'uploading',
86
+ }))
87
+
88
+ if (newAttachments && newAttachments.length > 0) {
89
+ setAttachments(prevAttachments => [...prevAttachments, ...newAttachments])
90
+
91
+ newAttachments.forEach(attachment => {
92
+ uploadApi
93
+ .uploadFile(attachment.file)
94
+ .then(({ id: uploadedFileId }) =>
95
+ apiClient.chat.post<ApiResource<MessageAttachmentResource>>({
96
+ url: `/me/conversations/${conversationId}/message_attachments`,
97
+ data: {
98
+ data: {
99
+ type: 'MessageAttachment',
100
+ attributes: { uploaded_file_id: uploadedFileId },
101
+ },
102
+ },
103
+ })
104
+ )
105
+ .then(({ data: { id: messageAttachmentId } }) => {
106
+ uploadState.current[attachment.file.name] = {
107
+ status: 'success',
108
+ id: messageAttachmentId,
109
+ }
110
+ setLastUploadId(messageAttachmentId)
111
+ })
112
+ .catch(() => {
113
+ uploadState.current[attachment.file.name] = {
114
+ status: 'error',
115
+ }
116
+ setLastUploadId(attachment.file.name)
117
+ })
118
+ })
119
+ }
120
+ },
121
+ [numberOfAttachments, uploadApi, apiClient.chat, conversationId]
122
+ )
123
+
124
+ useEffect(() => {
125
+ if (!lastUploadId) return
126
+
127
+ setLastUploadId(undefined)
128
+ setAttachments(
129
+ attachments.map(attachment => {
130
+ const state = uploadState.current[attachment.file.name]
131
+ if (state) {
132
+ return { ...attachment, id: state.id, status: state.status }
133
+ }
134
+ return attachment
135
+ })
136
+ )
137
+ }, [attachments, lastUploadId])
138
+
139
+ const removeAttachment = useCallback((attachment: FileAttachment) => {
140
+ setAttachments(prevAttachments =>
141
+ prevAttachments.filter(a => a.file.uri !== attachment.file.uri)
142
+ )
143
+ }, [])
144
+
145
+ const reset = useCallback(() => {
146
+ setAttachments([])
147
+ setErrorMessage(null)
148
+ }, [])
149
+
150
+ const pendingUploads = attachments.filter(a => a.status === 'uploading').length > 0
151
+
152
+ const attachmentIds = useMemo(
153
+ () => attachments.filter(a => a.status === 'success' && a.id).map(a => a.id as string),
154
+ [attachments]
155
+ )
156
+
157
+ return {
158
+ attachments,
159
+ attachmentIds,
160
+ handleFilesAttached,
161
+ removeAttachment,
162
+ reset,
163
+ pendingUploads,
164
+ errorMessage,
165
+ remainingAttachable: MAX_NUMBER_OF_ATTACHMENTS - numberOfAttachments,
166
+ }
167
+ }
168
+
169
+ interface MessageAttachmentResource {
170
+ type: 'MessageAttachment'
171
+ id: string
172
+ uploadedFileId: string
173
+ filename: string
174
+ messageSortKey: string
175
+ metadata: Record<string, unknown>
176
+ checksum: string
177
+ contentType: string
178
+ byteSize: number
179
+ }
@@ -0,0 +1,67 @@
1
+ import { useContext, useMemo } from 'react'
2
+ import { ChatContext } from '../contexts/chat_context'
3
+ import { UploadUri } from '../utils/upload_uri'
4
+ import { Client } from '../utils'
5
+
6
+ export interface FileForUploadClient {
7
+ uri: string
8
+ name: string
9
+ type: string // Should be a MIME type
10
+ }
11
+
12
+ export const useUploadClient = () => {
13
+ const { session, onUnauthorizedResponse } = useContext(ChatContext)
14
+
15
+ const uri = useMemo(() => new UploadUri({ session }), [session])
16
+
17
+ const api = useMemo(
18
+ () =>
19
+ new UploadClient({
20
+ version: '2025-05-16', // not really needed, but required by the Client constructor
21
+ root: uri.baseUrl,
22
+ defaultHeaders: uri.headers,
23
+ onUnauthorizedResponse,
24
+ }),
25
+ [uri, onUnauthorizedResponse]
26
+ )
27
+
28
+ return api
29
+ }
30
+
31
+ class UploadClient extends Client {
32
+ async uploadFile(file: FileForUploadClient): Promise<UploadedResource> {
33
+ const formData = new FormData()
34
+ formData.append('file', file as unknown as Blob)
35
+
36
+ const response = await fetch(`${this.root}/v2/files`, {
37
+ method: 'POST',
38
+ headers: this.headers,
39
+ body: formData,
40
+ })
41
+
42
+ if (!response.ok) {
43
+ return this.handleNotOk(response)
44
+ }
45
+
46
+ const jsonResponse = await response.json()
47
+
48
+ return jsonResponse.data[0]
49
+ }
50
+ }
51
+
52
+ interface UploadedResource {
53
+ id: string
54
+ type: string
55
+ attributes: {
56
+ organization_id: string
57
+ person_id: string
58
+ md5: string
59
+ created_at: string
60
+ expires_at: string
61
+ source_ip: string
62
+ name: string
63
+ content_type: string
64
+ file_size: number
65
+ location: string
66
+ }
67
+ }
package/src/index.tsx CHANGED
@@ -4,6 +4,7 @@ export { DesignSystemScreen } from './screens'
4
4
  export * from './navigation'
5
5
  export * from './types'
6
6
  export * from './utils/client'
7
+ export { GroupConversations } from './components'
7
8
 
8
9
  export {
9
10
  TemporaryDefaultColorsType,
@@ -70,14 +70,11 @@ export const NewConversationStack = createNativeStackNavigator({
70
70
  },
71
71
  ConversationNew: {
72
72
  screen: ConversationNewScreen,
73
- options: ({ navigation, route }) => ({
73
+ options: ({ navigation }) => ({
74
74
  title: 'New conversation',
75
75
  headerLeft: () => null,
76
76
  headerRight: (props: NativeStackHeaderRightProps) => (
77
- <HeaderRightButton
78
- {...props}
79
- onPress={() => navigation.popTo('Conversations', route.params)}
80
- >
77
+ <HeaderRightButton {...props} onPress={() => navigation.getParent()?.goBack()}>
81
78
  Cancel
82
79
  </HeaderRightButton>
83
80
  ),
@@ -3,23 +3,24 @@ import React, { useCallback, useMemo, useState } from 'react'
3
3
  import { Platform, StyleSheet, TextInput, View } from 'react-native'
4
4
  import { Banner, ChildNotice, Heading, Text } from '../../../components'
5
5
  import { ActionButton } from '../../../components/display/action_button'
6
- import { useCurrentPerson, useSuspenseGet } from '../../../hooks'
7
- import { GroupsGroupResource } from '../../../types'
8
- import { Divider, FormList } from './form_list'
9
- import { pluralize } from '../../../utils'
10
6
  import { KeyboardView } from '../../../components/display/keyboard_view'
7
+ import { useCurrentPerson, useSuspenseGet, useTheme } from '../../../hooks'
11
8
  import {
12
- useGroupMembersForNewConversation,
13
9
  GroupMembersForNewConversationResult,
10
+ useGroupMembersForNewConversation,
14
11
  } from '../../../hooks/groups/use_group_members_for_new_conversation'
15
12
  import { useGroupsConversationCreate } from '../../../hooks/groups/use_groups_conversation_create'
16
- import { useTheme } from '../../../hooks'
13
+ import { GroupsGroupResource } from '../../../types'
14
+ import { GraphId } from '../../../types/resources/group_resource'
15
+ import { pluralize } from '../../../utils'
16
+ import { Divider, FormList } from './form_list'
17
17
 
18
18
  type GroupsFormProps = {
19
19
  groupId: number
20
+ chat_group_graph_id?: GraphId
20
21
  }
21
22
 
22
- export const GroupsForm = ({ groupId }: GroupsFormProps) => {
23
+ export const GroupsForm = ({ groupId, chat_group_graph_id }: GroupsFormProps) => {
23
24
  const navigation = useNavigation()
24
25
  const [title, setTitle] = useState<string>()
25
26
  const { data: group } = useSuspenseGet<GroupsGroupResource>({
@@ -36,16 +37,15 @@ export const GroupsForm = ({ groupId }: GroupsFormProps) => {
36
37
 
37
38
  const redirectToConversation = useCallback(
38
39
  (conversationId: number) => {
39
- // exit from the create stack
40
- navigation.getParent()?.goBack()
41
40
  // navigate to the conversation screen
42
41
  navigation.dispatch(
43
- StackActions.push('Conversation', {
42
+ StackActions.popTo('Conversation', {
44
43
  conversation_id: conversationId,
44
+ chat_group_graph_id,
45
45
  })
46
46
  )
47
47
  },
48
- [navigation]
48
+ [chat_group_graph_id, navigation]
49
49
  )
50
50
 
51
51
  const { mutate: handleSave } = useGroupsConversationCreate({
@@ -16,11 +16,15 @@ type ConversationNewScreenProps = StaticScreenProps<{
16
16
  }>
17
17
 
18
18
  export const ConversationNewScreen = ({ route }: ConversationNewScreenProps) => {
19
- const { group_id, team_ids, source_app_name, plan_id } = route.params
19
+ const { group_id, team_ids, source_app_name, plan_id, chat_group_graph_id } = route.params
20
20
 
21
21
  switch (source_app_name) {
22
22
  case 'Groups':
23
- return group_id ? <GroupsForm groupId={group_id} /> : <SourceAppErrorCard />
23
+ return group_id ? (
24
+ <GroupsForm groupId={group_id} chat_group_graph_id={chat_group_graph_id} />
25
+ ) : (
26
+ <SourceAppErrorCard />
27
+ )
24
28
  case 'Services':
25
29
  return <TeamsForm initialTeamIds={team_ids} initialPlanId={plan_id} />
26
30
  default:
@@ -2,6 +2,8 @@
2
2
  import { date as formatDate } from '@planningcenter/datetime-fmt'
3
3
  import { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'
4
4
  import {
5
+ CommonActions,
6
+ RouteProp,
5
7
  StaticScreenProps,
6
8
  useNavigation,
7
9
  useTheme as useNavigationTheme,
@@ -38,6 +40,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
38
40
  const { messages, refetch, isRefetching, fetchNextPage } = useConversationMessages({
39
41
  conversation_id,
40
42
  })
43
+ useEnsureConversationsRouteExists()
41
44
  const messagesWithSeparators = groupMessages(messages)
42
45
 
43
46
  // Seems to be necessary to define this way so we get the route picked up
@@ -70,7 +73,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
70
73
  onEndReached={() => fetchNextPage()}
71
74
  />
72
75
  <MessageForm.Root conversation={conversation}>
73
- {/* <MessageForm.AttachmentPicker /> */}
76
+ <MessageForm.AttachmentPicker />
74
77
  <MessageForm.Commands />
75
78
  <MessageForm.TextInput />
76
79
  <MessageForm.SubmitButton />
@@ -232,3 +235,30 @@ const useStyles = () => {
232
235
  },
233
236
  })
234
237
  }
238
+
239
+ /**
240
+ * useEnsureConversationsRouteExists
241
+ */
242
+ const useEnsureConversationsRouteExists = () => {
243
+ const navigation = useNavigation()
244
+ const { params } = useRoute<RouteProp<ConversationScreenProps['route']>>()
245
+
246
+ useEffect(() => {
247
+ const navigationState = navigation.getState()
248
+ const routes = navigationState?.routes || []
249
+ const conversationsRoute = routes.find(r => r.name === 'Conversations')
250
+
251
+ if (conversationsRoute) return
252
+
253
+ navigation.dispatch(state => {
254
+ return CommonActions.reset({
255
+ ...state,
256
+ routes: [
257
+ { name: 'Conversations', params: { chat_group_graph_id: params?.chat_group_graph_id } },
258
+ ...routes,
259
+ ],
260
+ index: state.index + 1,
261
+ })
262
+ })
263
+ }, [navigation, params?.chat_group_graph_id])
264
+ }
@@ -21,7 +21,7 @@ export function ConversationsScreen({ route }: ConversationScreenProps) {
21
21
  const canCreateConversations = useCanCreateConversations()
22
22
  const styles = useStyles()
23
23
 
24
- const { chat_group_graph_id } = route.params
24
+ const { chat_group_graph_id } = route.params || {}
25
25
  const { sourceAppName, sourceId } = destructureChatGroupGraphId(chat_group_graph_id)
26
26
 
27
27
  const handleNewConversationNavigation = () => {
@@ -1,11 +1,13 @@
1
1
  import { AudioAdapter } from './audio'
2
2
  import { ClipboardAdapter } from './clipboard'
3
+ import { ImagePickerAdapter } from './image_picker'
3
4
  import { VideoAdapter } from './video'
4
5
 
5
6
  type ChatConfigurations = {
6
7
  clipboard: ClipboardAdapter
7
8
  audio: AudioAdapter
8
9
  video: VideoAdapter
10
+ imagePicker: ImagePickerAdapter
9
11
  }
10
12
 
11
13
  export class ChatAdapters {
@@ -13,6 +15,7 @@ export class ChatAdapters {
13
15
  Clipboard = configurations.clipboard
14
16
  Audio = configurations.audio
15
17
  Video = configurations.video
18
+ ImagePicker = configurations.imagePicker
16
19
  }
17
20
  }
18
21
 
@@ -45,4 +48,15 @@ let Video: VideoAdapter = new VideoAdapter({
45
48
  ),
46
49
  })
47
50
 
48
- export { Clipboard, Audio, Video }
51
+ let ImagePicker: ImagePickerAdapter = new ImagePickerAdapter({
52
+ openCameraAsync: async () => {
53
+ methodMissing()
54
+ return { canceled: true, assets: null }
55
+ },
56
+ openImageLibraryAsync: async () => {
57
+ methodMissing()
58
+ return { canceled: true, assets: null }
59
+ },
60
+ })
61
+
62
+ export { Clipboard, Audio, Video, ImagePicker }
@@ -0,0 +1,31 @@
1
+ export type ImagePickerAsset = {
2
+ uri: string
3
+ assetId?: string | null
4
+ width: number
5
+ height: number
6
+ mimeType?: string
7
+ fileName?: string | null
8
+ fileSize?: number
9
+ }
10
+
11
+ type ImagePickerSuccessResult = {
12
+ canceled: false
13
+ assets: ImagePickerAsset[]
14
+ }
15
+
16
+ type ImagePickerCanceledResult = {
17
+ canceled: true
18
+ assets: null
19
+ }
20
+
21
+ export type ImagePickerResult = ImagePickerSuccessResult | ImagePickerCanceledResult
22
+
23
+ export class ImagePickerAdapter {
24
+ openCameraAsync: () => Promise<ImagePickerResult>
25
+ openImageLibraryAsync: () => Promise<ImagePickerResult>
26
+
27
+ constructor(methods: ImagePickerAdapter) {
28
+ this.openCameraAsync = methods.openCameraAsync
29
+ this.openImageLibraryAsync = methods.openImageLibraryAsync
30
+ }
31
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './clipboard'
2
2
  export * from './configuration'
3
3
  export * from './audio'
4
+ export * from './image_picker'
4
5
  export * from './video'
@@ -0,0 +1,69 @@
1
+ import DeviceInfo from 'react-native-device-info'
2
+ import { Session } from './session'
3
+ const brand = DeviceInfo.getBrand()
4
+ const model = DeviceInfo.getModel()
5
+ const systemName = DeviceInfo.getSystemName()
6
+ const systemVersion = DeviceInfo.getSystemVersion()
7
+ const readableVersion = DeviceInfo.getReadableVersion()
8
+ const appName = DeviceInfo.getApplicationName()
9
+
10
+ /**
11
+ * This is for accessing https://github.com/planningcenter/upload
12
+ */
13
+ export class UploadUri {
14
+ session: Session
15
+ app?: string
16
+
17
+ constructor({ session }: { session: Session }) {
18
+ this.session = session
19
+ }
20
+
21
+ get schema() {
22
+ if (this.env === 'development') {
23
+ return 'http'
24
+ } else {
25
+ return 'https'
26
+ }
27
+ }
28
+
29
+ get host() {
30
+ return `${this.subdomain}.${this.domain}.${this.tld}`
31
+ }
32
+
33
+ get subdomain() {
34
+ switch (this.env) {
35
+ case 'staging':
36
+ return 'upload-staging'
37
+ default:
38
+ return 'upload'
39
+ }
40
+ }
41
+
42
+ get domain(): 'pco' | 'planningcenteronline' {
43
+ return this.env === 'development' ? 'pco' : 'planningcenteronline'
44
+ }
45
+
46
+ get tld() {
47
+ switch (this.env) {
48
+ case 'development':
49
+ return 'test'
50
+ default:
51
+ return 'com'
52
+ }
53
+ }
54
+
55
+ get env() {
56
+ return this.session?.env || 'production'
57
+ }
58
+
59
+ get baseUrl() {
60
+ return `${this.schema}://${this.host}`
61
+ }
62
+
63
+ get headers() {
64
+ return {
65
+ 'User-Agent': `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,
66
+ Authorization: `Bearer ${this.session.token?.access_token}`,
67
+ }
68
+ }
69
+ }