@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.
- package/build/components/conversation/message_form/message_form_attachment_image.d.ts +13 -0
- package/build/components/conversation/message_form/message_form_attachment_image.d.ts.map +1 -0
- package/build/components/conversation/message_form/message_form_attachment_image.js +78 -0
- package/build/components/conversation/message_form/message_form_attachment_image.js.map +1 -0
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +128 -16
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversations/conversation_actions.d.ts +2 -2
- package/build/components/conversations/conversation_actions.d.ts.map +1 -1
- package/build/components/conversations/conversation_actions.js.map +1 -1
- package/build/components/conversations/conversation_preview.d.ts +3 -1
- package/build/components/conversations/conversation_preview.d.ts.map +1 -1
- package/build/components/conversations/conversation_preview.js +2 -2
- package/build/components/conversations/conversation_preview.js.map +1 -1
- package/build/components/group_conversation_list.d.ts +19 -0
- package/build/components/group_conversation_list.d.ts.map +1 -0
- package/build/components/group_conversation_list.js +48 -0
- package/build/components/group_conversation_list.js.map +1 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.d.ts.map +1 -1
- package/build/components/index.js +1 -0
- package/build/components/index.js.map +1 -1
- package/build/contexts/conversations_context.js +1 -1
- package/build/contexts/conversations_context.js.map +1 -1
- package/build/hooks/attachments/supported_extensions.d.ts +2 -0
- package/build/hooks/attachments/supported_extensions.d.ts.map +1 -0
- package/build/hooks/attachments/supported_extensions.js +48 -0
- package/build/hooks/attachments/supported_extensions.js.map +1 -0
- package/build/hooks/index.d.ts +4 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +4 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/use_api.d.ts +2 -2
- package/build/hooks/use_api.d.ts.map +1 -1
- package/build/hooks/use_api.js.map +1 -1
- package/build/hooks/use_attachment_uploader.d.ts +26 -0
- package/build/hooks/use_attachment_uploader.d.ts.map +1 -0
- package/build/hooks/use_attachment_uploader.js +111 -0
- package/build/hooks/use_attachment_uploader.js.map +1 -0
- package/build/hooks/use_upload_client.d.ts +28 -0
- package/build/hooks/use_upload_client.d.ts.map +1 -0
- package/build/hooks/use_upload_client.js +32 -0
- package/build/hooks/use_upload_client.js.map +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/navigation/index.d.ts +2 -2
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +2 -2
- package/build/navigation/index.js.map +1 -1
- package/build/screens/conversation_new/components/groups_form.d.ts +3 -1
- package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/groups_form.js +7 -9
- package/build/screens/conversation_new/components/groups_form.js.map +1 -1
- package/build/screens/conversation_new/conversation_new_screen.d.ts.map +1 -1
- package/build/screens/conversation_new/conversation_new_screen.js +2 -2
- package/build/screens/conversation_new/conversation_new_screen.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +27 -2
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversations/conversations_screen.js +1 -1
- package/build/screens/conversations/conversations_screen.js.map +1 -1
- package/build/utils/native_adapters/configuration.d.ts +4 -1
- package/build/utils/native_adapters/configuration.d.ts.map +1 -1
- package/build/utils/native_adapters/configuration.js +13 -1
- package/build/utils/native_adapters/configuration.js.map +1 -1
- package/build/utils/native_adapters/image_picker.d.ts +25 -0
- package/build/utils/native_adapters/image_picker.d.ts.map +1 -0
- package/build/utils/native_adapters/image_picker.js +9 -0
- package/build/utils/native_adapters/image_picker.js.map +1 -0
- package/build/utils/native_adapters/index.d.ts +1 -0
- package/build/utils/native_adapters/index.d.ts.map +1 -1
- package/build/utils/native_adapters/index.js +1 -0
- package/build/utils/native_adapters/index.js.map +1 -1
- package/build/utils/upload_uri.d.ts +23 -0
- package/build/utils/upload_uri.d.ts.map +1 -0
- package/build/utils/upload_uri.js +60 -0
- package/build/utils/upload_uri.js.map +1 -0
- package/package.json +2 -2
- package/src/components/conversation/message_form/message_form_attachment_image.tsx +121 -0
- package/src/components/conversation/message_form.tsx +197 -31
- package/src/components/conversations/conversation_actions.tsx +2 -2
- package/src/components/conversations/conversation_preview.tsx +8 -2
- package/src/components/group_conversation_list.tsx +82 -0
- package/src/components/index.tsx +1 -0
- package/src/contexts/conversations_context.tsx +1 -1
- package/src/hooks/attachments/supported_extensions.ts +47 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use_api.ts +2 -2
- package/src/hooks/use_attachment_uploader.ts +179 -0
- package/src/hooks/use_upload_client.ts +67 -0
- package/src/index.tsx +1 -0
- package/src/navigation/index.tsx +2 -5
- package/src/screens/conversation_new/components/groups_form.tsx +11 -11
- package/src/screens/conversation_new/conversation_new_screen.tsx +6 -2
- package/src/screens/conversation_screen.tsx +31 -1
- package/src/screens/conversations/conversations_screen.tsx +1 -1
- package/src/utils/native_adapters/configuration.ts +15 -1
- package/src/utils/native_adapters/image_picker.ts +31 -0
- package/src/utils/native_adapters/index.ts +1 -0
- 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
|
+
]
|
package/src/hooks/index.ts
CHANGED
|
@@ -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'
|
package/src/hooks/use_api.ts
CHANGED
|
@@ -36,14 +36,14 @@ type NextMeta = Partial<{
|
|
|
36
36
|
idLt: string
|
|
37
37
|
}>
|
|
38
38
|
|
|
39
|
-
export type
|
|
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?:
|
|
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
package/src/navigation/index.tsx
CHANGED
|
@@ -70,14 +70,11 @@ export const NewConversationStack = createNativeStackNavigator({
|
|
|
70
70
|
},
|
|
71
71
|
ConversationNew: {
|
|
72
72
|
screen: ConversationNewScreen,
|
|
73
|
-
options: ({ navigation
|
|
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 {
|
|
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.
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|