@planningcenter/chat-react-native 3.32.1-rc.0 → 3.33.0-rc.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.
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +22 -1
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/display/emoji_avatar.d.ts.map +1 -1
- package/build/components/display/emoji_avatar.js +2 -0
- package/build/components/display/emoji_avatar.js.map +1 -1
- package/build/components/display/icon_avatar.d.ts.map +1 -1
- package/build/components/display/icon_avatar.js +2 -0
- package/build/components/display/icon_avatar.js.map +1 -1
- package/build/components/display/utils/avatar_gradient_colors.d.ts +3 -0
- package/build/components/display/utils/avatar_gradient_colors.d.ts.map +1 -1
- package/build/components/display/utils/avatar_gradient_colors.js +8 -3
- package/build/components/display/utils/avatar_gradient_colors.js.map +1 -1
- package/build/components/page/error_boundary.d.ts.map +1 -1
- package/build/components/page/error_boundary.js +13 -10
- package/build/components/page/error_boundary.js.map +1 -1
- package/build/components/primitive/avatar_primitive.d.ts +3 -1
- package/build/components/primitive/avatar_primitive.d.ts.map +1 -1
- package/build/components/primitive/avatar_primitive.js +10 -2
- package/build/components/primitive/avatar_primitive.js.map +1 -1
- package/build/contexts/api_provider.d.ts.map +1 -1
- package/build/contexts/api_provider.js +2 -0
- package/build/contexts/api_provider.js.map +1 -1
- package/build/hooks/attachments/fallback_chat_configuration.d.ts +4 -0
- package/build/hooks/attachments/fallback_chat_configuration.d.ts.map +1 -0
- package/build/hooks/attachments/fallback_chat_configuration.js +59 -0
- package/build/hooks/attachments/fallback_chat_configuration.js.map +1 -0
- package/build/hooks/groups/use_groups_conversation_create.d.ts.map +1 -1
- package/build/hooks/groups/use_groups_conversation_create.js +1 -1
- package/build/hooks/groups/use_groups_conversation_create.js.map +1 -1
- package/build/hooks/services/use_find_or_create_services_conversation.d.ts +43 -11
- package/build/hooks/services/use_find_or_create_services_conversation.d.ts.map +1 -1
- package/build/hooks/services/use_find_or_create_services_conversation.js +5 -5
- package/build/hooks/services/use_find_or_create_services_conversation.js.map +1 -1
- package/build/hooks/use_attachment_uploader.d.ts.map +1 -1
- package/build/hooks/use_attachment_uploader.js +39 -14
- package/build/hooks/use_attachment_uploader.js.map +1 -1
- package/build/hooks/use_chat_configuration.d.ts +6 -0
- package/build/hooks/use_chat_configuration.d.ts.map +1 -0
- package/build/hooks/use_chat_configuration.js +41 -0
- package/build/hooks/use_chat_configuration.js.map +1 -0
- package/build/hooks/use_conversation_avatar_update.d.ts +26 -0
- package/build/hooks/use_conversation_avatar_update.d.ts.map +1 -0
- package/build/hooks/use_conversation_avatar_update.js +130 -0
- package/build/hooks/use_conversation_avatar_update.js.map +1 -0
- package/build/hooks/use_features.d.ts +1 -0
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +1 -0
- package/build/hooks/use_features.js.map +1 -1
- package/build/navigation/index.d.ts +16 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +9 -0
- package/build/navigation/index.js.map +1 -1
- package/build/screens/avatar_picker/avatar_picker_screen.d.ts +12 -0
- package/build/screens/avatar_picker/avatar_picker_screen.d.ts.map +1 -0
- package/build/screens/avatar_picker/avatar_picker_screen.js +193 -0
- package/build/screens/avatar_picker/avatar_picker_screen.js.map +1 -0
- package/build/screens/avatar_picker/avatar_picker_state.d.ts +38 -0
- package/build/screens/avatar_picker/avatar_picker_state.d.ts.map +1 -0
- package/build/screens/avatar_picker/avatar_picker_state.js +101 -0
- package/build/screens/avatar_picker/avatar_picker_state.js.map +1 -0
- package/build/screens/avatar_picker/avatar_preview.d.ts +9 -0
- package/build/screens/avatar_picker/avatar_preview.d.ts.map +1 -0
- package/build/screens/avatar_picker/avatar_preview.js +39 -0
- package/build/screens/avatar_picker/avatar_preview.js.map +1 -0
- package/build/screens/avatar_picker/color_picker.d.ts +9 -0
- package/build/screens/avatar_picker/color_picker.d.ts.map +1 -0
- package/build/screens/avatar_picker/color_picker.js +53 -0
- package/build/screens/avatar_picker/color_picker.js.map +1 -0
- package/build/screens/avatar_picker/constants.d.ts +3 -0
- package/build/screens/avatar_picker/constants.d.ts.map +1 -0
- package/build/screens/avatar_picker/constants.js +53 -0
- package/build/screens/avatar_picker/constants.js.map +1 -0
- package/build/screens/avatar_picker/emoji_tab.d.ts +7 -0
- package/build/screens/avatar_picker/emoji_tab.d.ts.map +1 -0
- package/build/screens/avatar_picker/emoji_tab.js +55 -0
- package/build/screens/avatar_picker/emoji_tab.js.map +1 -0
- package/build/screens/avatar_picker/icon_grid.d.ts +8 -0
- package/build/screens/avatar_picker/icon_grid.d.ts.map +1 -0
- package/build/screens/avatar_picker/icon_grid.js +48 -0
- package/build/screens/avatar_picker/icon_grid.js.map +1 -0
- package/build/screens/avatar_picker/upload_tab.d.ts +9 -0
- package/build/screens/avatar_picker/upload_tab.d.ts.map +1 -0
- package/build/screens/avatar_picker/upload_tab.js +39 -0
- package/build/screens/avatar_picker/upload_tab.js.map +1 -0
- package/build/screens/conversation_details_screen.d.ts.map +1 -1
- package/build/screens/conversation_details_screen.js +37 -1
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_new/components/avatar_selection_row.d.ts +12 -0
- package/build/screens/conversation_new/components/avatar_selection_row.d.ts.map +1 -0
- package/build/screens/conversation_new/components/avatar_selection_row.js +60 -0
- package/build/screens/conversation_new/components/avatar_selection_row.js.map +1 -0
- package/build/screens/conversation_new/components/gender_filter_toggle.d.ts.map +1 -1
- package/build/screens/conversation_new/components/gender_filter_toggle.js +3 -9
- package/build/screens/conversation_new/components/gender_filter_toggle.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 +22 -8
- package/build/screens/conversation_new/components/groups_form.js.map +1 -1
- package/build/screens/conversation_new/components/services_form.d.ts +3 -1
- package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/services_form.js +22 -8
- package/build/screens/conversation_new/components/services_form.js.map +1 -1
- package/build/screens/conversation_new/conversation_new_screen.d.ts +2 -0
- package/build/screens/conversation_new/conversation_new_screen.d.ts.map +1 -1
- package/build/screens/conversation_new/conversation_new_screen.js +3 -3
- package/build/screens/conversation_new/conversation_new_screen.js.map +1 -1
- package/build/screens/team_conversation_screen.d.ts.map +1 -1
- package/build/screens/team_conversation_screen.js +1 -1
- package/build/screens/team_conversation_screen.js.map +1 -1
- package/build/types/resources/chat_configuration_resource.d.ts +8 -0
- package/build/types/resources/chat_configuration_resource.d.ts.map +1 -0
- package/build/types/resources/chat_configuration_resource.js +2 -0
- package/build/types/resources/chat_configuration_resource.js.map +1 -0
- package/build/utils/auth_events.d.ts +7 -0
- package/build/utils/auth_events.d.ts.map +1 -0
- package/build/utils/auth_events.js +17 -0
- package/build/utils/auth_events.js.map +1 -0
- package/build/utils/native_adapters/configuration.d.ts +3 -0
- package/build/utils/native_adapters/configuration.d.ts.map +1 -1
- package/build/utils/native_adapters/configuration.js +8 -0
- package/build/utils/native_adapters/configuration.js.map +1 -1
- package/build/utils/native_adapters/document_picker.d.ts +21 -0
- package/build/utils/native_adapters/document_picker.d.ts.map +1 -0
- package/build/utils/native_adapters/document_picker.js +7 -0
- package/build/utils/native_adapters/document_picker.js.map +1 -0
- package/build/utils/native_adapters/image_picker.d.ts +7 -1
- package/build/utils/native_adapters/image_picker.d.ts.map +1 -1
- package/build/utils/native_adapters/image_picker.js.map +1 -1
- 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/request/get_chat_configuration.d.ts +10 -0
- package/build/utils/request/get_chat_configuration.d.ts.map +1 -0
- package/build/utils/request/get_chat_configuration.js +21 -0
- package/build/utils/request/get_chat_configuration.js.map +1 -0
- package/package.json +4 -3
- package/src/__tests__/hooks/use_attachment_uploader.test.tsx +219 -0
- package/src/__tests__/hooks/use_chat_configuration.test.tsx +80 -0
- package/src/__tests__/utils/native_adapters/configuration.ts +25 -1
- package/src/components/conversation/message_form.tsx +39 -1
- package/src/components/display/emoji_avatar.tsx +7 -2
- package/src/components/display/icon_avatar.tsx +7 -2
- package/src/components/display/utils/avatar_gradient_colors.ts +10 -3
- package/src/components/page/error_boundary.tsx +16 -9
- package/src/components/primitive/avatar_primitive.tsx +11 -2
- package/src/contexts/api_provider.tsx +3 -0
- package/src/hooks/attachments/fallback_chat_configuration.ts +61 -0
- package/src/hooks/groups/use_groups_conversation_create.ts +2 -1
- package/src/hooks/services/use_find_or_create_services_conversation.ts +7 -7
- package/src/hooks/use_attachment_uploader.ts +39 -15
- package/src/hooks/use_chat_configuration.ts +54 -0
- package/src/hooks/use_conversation_avatar_update.ts +163 -0
- package/src/hooks/use_features.ts +1 -0
- package/src/navigation/index.tsx +13 -0
- package/src/screens/avatar_picker/__tests__/avatar_picker_state.test.ts +157 -0
- package/src/screens/avatar_picker/avatar_picker_screen.tsx +312 -0
- package/src/screens/avatar_picker/avatar_picker_state.ts +141 -0
- package/src/screens/avatar_picker/avatar_preview.tsx +46 -0
- package/src/screens/avatar_picker/color_picker.tsx +91 -0
- package/src/screens/avatar_picker/constants.ts +53 -0
- package/src/screens/avatar_picker/emoji_tab.tsx +76 -0
- package/src/screens/avatar_picker/icon_grid.tsx +81 -0
- package/src/screens/avatar_picker/upload_tab.tsx +62 -0
- package/src/screens/conversation_details_screen.tsx +60 -1
- package/src/screens/conversation_new/components/avatar_selection_row.tsx +82 -0
- package/src/screens/conversation_new/components/gender_filter_toggle.tsx +3 -9
- package/src/screens/conversation_new/components/groups_form.tsx +33 -6
- package/src/screens/conversation_new/components/services_form.tsx +37 -6
- package/src/screens/conversation_new/conversation_new_screen.tsx +17 -3
- package/src/screens/team_conversation_screen.tsx +2 -1
- package/src/types/resources/chat_configuration_resource.ts +11 -0
- package/src/utils/auth_events.ts +21 -0
- package/src/utils/native_adapters/configuration.ts +10 -0
- package/src/utils/native_adapters/document_picker.ts +26 -0
- package/src/utils/native_adapters/image_picker.ts +8 -1
- package/src/utils/native_adapters/index.ts +1 -0
- package/src/utils/request/get_chat_configuration.ts +23 -0
- package/build/hooks/attachments/supported_extensions.d.ts +0 -2
- package/build/hooks/attachments/supported_extensions.d.ts.map +0 -1
- package/build/hooks/attachments/supported_extensions.js +0 -48
- package/build/hooks/attachments/supported_extensions.js.map +0 -1
- package/src/hooks/attachments/supported_extensions.ts +0 -47
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-hooks'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { useApiClient } from '../../hooks/use_api_client'
|
|
5
|
+
import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
|
|
6
|
+
import { useChatConfiguration } from '../../hooks/use_chat_configuration'
|
|
7
|
+
import { useUploadClient } from '../../hooks/use_upload_client'
|
|
8
|
+
import { FileAttachment } from '../../types/resources/denormalized_attachment_resource_for_create'
|
|
9
|
+
|
|
10
|
+
jest.mock('../../hooks/use_api_client')
|
|
11
|
+
jest.mock('../../hooks/use_upload_client')
|
|
12
|
+
jest.mock('../../hooks/use_chat_configuration')
|
|
13
|
+
|
|
14
|
+
const mockedUseApiClient = useApiClient as jest.MockedFunction<typeof useApiClient>
|
|
15
|
+
const mockedUseUploadClient = useUploadClient as jest.MockedFunction<typeof useUploadClient>
|
|
16
|
+
const mockedUseChatConfiguration = useChatConfiguration as jest.MockedFunction<
|
|
17
|
+
typeof useChatConfiguration
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
const createWrapper = () => {
|
|
21
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
22
|
+
return ({ children }: { children: React.ReactNode }) => (
|
|
23
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const setChatConfiguration = (overrides: Partial<ReturnType<typeof useChatConfiguration>> = {}) => {
|
|
28
|
+
mockedUseChatConfiguration.mockReturnValue({
|
|
29
|
+
allowedFileExtensions: ['.pdf', '.jpg'],
|
|
30
|
+
maxFileSizeInBytes: 50 * 1024 * 1024,
|
|
31
|
+
maxAttachmentsPerMessage: 10,
|
|
32
|
+
...overrides,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const renderUploader = (opts?: { draftAttachments?: FileAttachment[] }) =>
|
|
37
|
+
renderHook(() => useAttachmentUploader({ conversationId: 1, ...opts }), {
|
|
38
|
+
wrapper: createWrapper(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const makeFile = (
|
|
42
|
+
overrides: Partial<
|
|
43
|
+
Parameters<ReturnType<typeof useAttachmentUploader>['handleFilesAttached']>[0][number]
|
|
44
|
+
> = {}
|
|
45
|
+
) => ({
|
|
46
|
+
uri: 'file:///tmp/example.pdf',
|
|
47
|
+
name: 'example.pdf',
|
|
48
|
+
type: 'application/pdf',
|
|
49
|
+
size: 1024,
|
|
50
|
+
...overrides,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const draftAttachment = (name: string): FileAttachment => ({
|
|
54
|
+
file: { uri: `file:///tmp/${name}`, name, type: 'application/pdf', size: 1024 },
|
|
55
|
+
status: 'success',
|
|
56
|
+
uploadedAt: 0,
|
|
57
|
+
id: `att-${name}`,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
setChatConfiguration()
|
|
62
|
+
mockedUseApiClient.mockReturnValue({
|
|
63
|
+
chat: { post: jest.fn().mockResolvedValue({ data: { id: 'msg-attachment-1' } }) },
|
|
64
|
+
} as any)
|
|
65
|
+
mockedUseUploadClient.mockReturnValue({
|
|
66
|
+
uploadFile: jest.fn().mockResolvedValue({ id: 'uploaded-1' }),
|
|
67
|
+
} as any)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
jest.clearAllMocks()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('useAttachmentUploader', () => {
|
|
75
|
+
describe('extension validation', () => {
|
|
76
|
+
it('rejects a disallowed extension and lists it in the error', () => {
|
|
77
|
+
const { result } = renderUploader()
|
|
78
|
+
|
|
79
|
+
act(() => {
|
|
80
|
+
result.current.handleFilesAttached([makeFile({ name: 'evil.exe' })])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(result.current.errorMessage).toContain(
|
|
84
|
+
'The following file types are not supported: exe'
|
|
85
|
+
)
|
|
86
|
+
expect(result.current.attachments).toHaveLength(0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('accepts a matching extension regardless of casing', async () => {
|
|
90
|
+
const { result } = renderUploader()
|
|
91
|
+
|
|
92
|
+
await act(async () => {
|
|
93
|
+
result.current.handleFilesAttached([makeFile({ name: 'Shouting.PDF' })])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(result.current.errorMessage).toBeNull()
|
|
97
|
+
expect(result.current.attachments).toHaveLength(1)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('file size validation', () => {
|
|
102
|
+
it('rejects files that exceed maxFileSizeInBytes', () => {
|
|
103
|
+
setChatConfiguration({ maxFileSizeInBytes: 50 * 1024 * 1024 })
|
|
104
|
+
const { result } = renderUploader()
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
result.current.handleFilesAttached([makeFile({ size: 50 * 1024 * 1024 + 1 })])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(result.current.errorMessage).toBe('File size exceeds 50 MB')
|
|
111
|
+
expect(result.current.attachments).toHaveLength(0)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('formats an integer MB limit without a trailing decimal', () => {
|
|
115
|
+
setChatConfiguration({ maxFileSizeInBytes: 25 * 1024 * 1024 })
|
|
116
|
+
const { result } = renderUploader()
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
result.current.handleFilesAttached([makeFile({ size: 25 * 1024 * 1024 + 1 })])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(result.current.errorMessage).toBe('File size exceeds 25 MB')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('formats a fractional MB limit to one decimal', () => {
|
|
126
|
+
// 50.5 MB — server could in principle return a non-multiple-of-MB cap.
|
|
127
|
+
setChatConfiguration({ maxFileSizeInBytes: Math.round(50.5 * 1024 * 1024) })
|
|
128
|
+
const { result } = renderUploader()
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
result.current.handleFilesAttached([makeFile({ size: 51 * 1024 * 1024 })])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
expect(result.current.errorMessage).toBe('File size exceeds 50.5 MB')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('attachment count validation', () => {
|
|
139
|
+
it('rejects a batch that would push past maxAttachmentsPerMessage', () => {
|
|
140
|
+
setChatConfiguration({ maxAttachmentsPerMessage: 2 })
|
|
141
|
+
const { result } = renderUploader()
|
|
142
|
+
|
|
143
|
+
act(() => {
|
|
144
|
+
result.current.handleFilesAttached([
|
|
145
|
+
makeFile({ uri: 'file:///a.pdf', name: 'a.pdf' }),
|
|
146
|
+
makeFile({ uri: 'file:///b.pdf', name: 'b.pdf' }),
|
|
147
|
+
makeFile({ uri: 'file:///c.pdf', name: 'c.pdf' }),
|
|
148
|
+
])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
expect(result.current.errorMessage).toBe("You can't attach more than 2 files at once.")
|
|
152
|
+
expect(result.current.attachments).toHaveLength(0)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('remainingAttachable', () => {
|
|
157
|
+
it('reports how many more files can be attached', () => {
|
|
158
|
+
setChatConfiguration({ maxAttachmentsPerMessage: 5 })
|
|
159
|
+
const { result } = renderUploader({
|
|
160
|
+
draftAttachments: [draftAttachment('a.pdf'), draftAttachment('b.pdf')],
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(result.current.remainingAttachable).toBe(3)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('clamps to 0 when the draft already exceeds the server limit', () => {
|
|
167
|
+
// Simulates a saved draft whose attachments outnumber a since-lowered
|
|
168
|
+
// server cap — the value must never go negative.
|
|
169
|
+
setChatConfiguration({ maxAttachmentsPerMessage: 1 })
|
|
170
|
+
const { result } = renderUploader({
|
|
171
|
+
draftAttachments: [draftAttachment('a.pdf'), draftAttachment('b.pdf')],
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(result.current.remainingAttachable).toBe(0)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('upload errors', () => {
|
|
179
|
+
const rejectingResponse = (body: unknown, status = 500) => {
|
|
180
|
+
const response = { status, clone: () => response, json: async () => body }
|
|
181
|
+
return response as unknown as Response
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
it('shows a generic error message when the upload fails', async () => {
|
|
185
|
+
mockedUseApiClient.mockReturnValue({
|
|
186
|
+
chat: { post: jest.fn().mockRejectedValue(rejectingResponse({})) },
|
|
187
|
+
} as any)
|
|
188
|
+
|
|
189
|
+
const { result } = renderUploader()
|
|
190
|
+
|
|
191
|
+
await act(async () => {
|
|
192
|
+
result.current.handleFilesAttached([makeFile()])
|
|
193
|
+
})
|
|
194
|
+
await act(async () => {
|
|
195
|
+
await Promise.resolve()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(result.current.errorMessage).toBe('This file could not be uploaded.')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('does not overwrite the error message when the upload is flagged', async () => {
|
|
202
|
+
mockedUseUploadClient.mockReturnValue({
|
|
203
|
+
uploadFile: jest.fn().mockRejectedValue({ code: 'image_flagged' }),
|
|
204
|
+
} as any)
|
|
205
|
+
|
|
206
|
+
const { result } = renderUploader()
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
result.current.handleFilesAttached([makeFile()])
|
|
210
|
+
})
|
|
211
|
+
await act(async () => {
|
|
212
|
+
await Promise.resolve()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
expect(result.current.errorMessage).toBeNull()
|
|
216
|
+
expect(result.current.flaggedAttachmentCount).toBe(1)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-hooks'
|
|
3
|
+
import React, { Suspense } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
FALLBACK_ALLOWED_FILE_EXTENSIONS,
|
|
6
|
+
FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
|
|
7
|
+
FALLBACK_MAX_FILE_SIZE_IN_BYTES,
|
|
8
|
+
} from '../../hooks/attachments/fallback_chat_configuration'
|
|
9
|
+
import * as useApiClientModule from '../../hooks/use_api_client'
|
|
10
|
+
import { useChatConfiguration } from '../../hooks/use_chat_configuration'
|
|
11
|
+
|
|
12
|
+
const createWrapper = () => {
|
|
13
|
+
const queryClient = new QueryClient({
|
|
14
|
+
defaultOptions: { queries: { retry: false } },
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return ({ children }: { children: React.ReactNode }) => (
|
|
18
|
+
<QueryClientProvider client={queryClient}>
|
|
19
|
+
<Suspense fallback={null}>{children}</Suspense>
|
|
20
|
+
</QueryClientProvider>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const waitForQuery = async () => {
|
|
25
|
+
await act(async () => {
|
|
26
|
+
await Promise.resolve()
|
|
27
|
+
await Promise.resolve()
|
|
28
|
+
await Promise.resolve()
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const mockApiClient = (getImpl: () => Promise<unknown>) => {
|
|
33
|
+
jest.spyOn(useApiClientModule, 'useApiClient').mockReturnValue({
|
|
34
|
+
chat: { get: jest.fn(getImpl) },
|
|
35
|
+
} as any)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('useChatConfiguration', () => {
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
jest.restoreAllMocks()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns server-provided values when the API succeeds', async () => {
|
|
44
|
+
mockApiClient(() =>
|
|
45
|
+
Promise.resolve({
|
|
46
|
+
data: {
|
|
47
|
+
type: 'ChatConfiguration',
|
|
48
|
+
id: 'current',
|
|
49
|
+
allowedFileExtensions: ['.pdf', '.jpg'],
|
|
50
|
+
maxFileSizeInBytes: 1000,
|
|
51
|
+
maxAttachmentsPerMessage: 3,
|
|
52
|
+
},
|
|
53
|
+
links: {},
|
|
54
|
+
meta: {},
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const { result } = renderHook(() => useChatConfiguration(), { wrapper: createWrapper() })
|
|
59
|
+
await waitForQuery()
|
|
60
|
+
|
|
61
|
+
expect(result.current).toEqual({
|
|
62
|
+
allowedFileExtensions: ['.pdf', '.jpg'],
|
|
63
|
+
maxFileSizeInBytes: 1000,
|
|
64
|
+
maxAttachmentsPerMessage: 3,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('returns fallback values when the API rejects', async () => {
|
|
69
|
+
mockApiClient(() => Promise.reject(new Error('boom')))
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(() => useChatConfiguration(), { wrapper: createWrapper() })
|
|
72
|
+
await waitForQuery()
|
|
73
|
+
|
|
74
|
+
expect(result.current).toEqual({
|
|
75
|
+
allowedFileExtensions: FALLBACK_ALLOWED_FILE_EXTENSIONS,
|
|
76
|
+
maxFileSizeInBytes: FALLBACK_MAX_FILE_SIZE_IN_BYTES,
|
|
77
|
+
maxAttachmentsPerMessage: FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -4,11 +4,17 @@ import {
|
|
|
4
4
|
AudioAdapter,
|
|
5
5
|
Clipboard,
|
|
6
6
|
ClipboardAdapter,
|
|
7
|
+
DocumentPickerAdapter,
|
|
7
8
|
ImagePickerAdapter,
|
|
8
9
|
LinkingAdapter,
|
|
9
10
|
VideoAdapter,
|
|
10
11
|
} from '../../../utils/native_adapters'
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
ChatAdapters,
|
|
14
|
+
DocumentPicker,
|
|
15
|
+
Linking,
|
|
16
|
+
Haptic,
|
|
17
|
+
} from '../../../utils/native_adapters/configuration'
|
|
12
18
|
import { HapticAdapter } from '../../../utils/native_adapters/haptic'
|
|
13
19
|
import { VideoPlayerHandle, VideoPlayerProps } from '../../../utils/native_adapters/video'
|
|
14
20
|
|
|
@@ -104,6 +110,24 @@ describe('ChatAdapters', () => {
|
|
|
104
110
|
})
|
|
105
111
|
})
|
|
106
112
|
|
|
113
|
+
describe('document picker adapter', () => {
|
|
114
|
+
it('uses the configured adapter when provided', () => {
|
|
115
|
+
const documentPicker = new DocumentPickerAdapter({
|
|
116
|
+
openAsync: jest.fn(async () => ({ canceled: true, assets: null })),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
ChatAdapters.configure({
|
|
120
|
+
clipboard,
|
|
121
|
+
audio,
|
|
122
|
+
video,
|
|
123
|
+
imagePicker,
|
|
124
|
+
documentPicker,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(DocumentPicker).toEqual(documentPicker)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
107
131
|
describe('haptic adapter', () => {
|
|
108
132
|
it('should configure the haptic adapter', () => {
|
|
109
133
|
ChatAdapters.configure({
|
|
@@ -36,7 +36,13 @@ import {
|
|
|
36
36
|
platformFontWeightMedium,
|
|
37
37
|
platformPressedOpacityStyle,
|
|
38
38
|
} from '../../utils'
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
DocumentPicker,
|
|
41
|
+
DocumentPickerResult,
|
|
42
|
+
Haptic,
|
|
43
|
+
ImagePicker,
|
|
44
|
+
ImagePickerResult,
|
|
45
|
+
} from '../../utils/native_adapters'
|
|
40
46
|
import { tokens } from '../../vendor/tapestry/tokens'
|
|
41
47
|
import { Button } from '../display/button'
|
|
42
48
|
import BannerPrimitive from '../primitive/banner_primitive'
|
|
@@ -522,6 +528,21 @@ function MessageFormAttachmentPicker() {
|
|
|
522
528
|
attachmentUploader?.handleFilesAttached(filteredAssets)
|
|
523
529
|
}
|
|
524
530
|
|
|
531
|
+
function uploadDocumentPickerResult(result: DocumentPickerResult) {
|
|
532
|
+
if (result.canceled) return
|
|
533
|
+
|
|
534
|
+
const filteredAssets = result.assets
|
|
535
|
+
.filter(asset => asset.size != null && asset.uri)
|
|
536
|
+
.map(asset => ({
|
|
537
|
+
uri: asset.uri,
|
|
538
|
+
name: asset.name,
|
|
539
|
+
type: asset.mimeType || 'application/octet-stream',
|
|
540
|
+
size: asset.size as number,
|
|
541
|
+
}))
|
|
542
|
+
|
|
543
|
+
attachmentUploader?.handleFilesAttached(filteredAssets)
|
|
544
|
+
}
|
|
545
|
+
|
|
525
546
|
const openCamera = async () => {
|
|
526
547
|
setIsOpen(false)
|
|
527
548
|
let result = await ImagePicker.openCameraAsync()
|
|
@@ -538,6 +559,14 @@ function MessageFormAttachmentPicker() {
|
|
|
538
559
|
}
|
|
539
560
|
}
|
|
540
561
|
|
|
562
|
+
const pickFile = async () => {
|
|
563
|
+
setIsOpen(false)
|
|
564
|
+
let result = await DocumentPicker.openAsync()
|
|
565
|
+
if (!result.canceled) {
|
|
566
|
+
uploadDocumentPickerResult(result)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
541
570
|
if (usingGiphy || currentlyEditingMessage) {
|
|
542
571
|
return null
|
|
543
572
|
}
|
|
@@ -564,6 +593,15 @@ function MessageFormAttachmentPicker() {
|
|
|
564
593
|
onPress={pickImage}
|
|
565
594
|
style={styles.attachmentPickerButton}
|
|
566
595
|
/>
|
|
596
|
+
<IconButton
|
|
597
|
+
accessibilityLabel="Attach a file"
|
|
598
|
+
accessibilityHint="Opens your files to attach documents"
|
|
599
|
+
size="lg"
|
|
600
|
+
appearance="neutral"
|
|
601
|
+
name="general.blankFile"
|
|
602
|
+
onPress={pickFile}
|
|
603
|
+
style={styles.attachmentPickerButton}
|
|
604
|
+
/>
|
|
567
605
|
</View>
|
|
568
606
|
)}
|
|
569
607
|
<IconButton
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { StyleSheet, Text, View } from 'react-native'
|
|
3
3
|
import LinearGradient from 'react-native-linear-gradient'
|
|
4
|
-
import AvatarPrimitive, {
|
|
4
|
+
import AvatarPrimitive, {
|
|
5
|
+
type AvatarRootProps,
|
|
6
|
+
type AvatarSize,
|
|
7
|
+
} from '../primitive/avatar_primitive'
|
|
5
8
|
import { getAvatarGradientProps } from './utils/avatar_gradient_colors'
|
|
6
9
|
|
|
7
|
-
const EMOJI_SIZE: Record<
|
|
10
|
+
const EMOJI_SIZE: Record<AvatarSize, number> = {
|
|
8
11
|
xs: 8,
|
|
9
12
|
sm: 10,
|
|
10
13
|
md: 14,
|
|
11
14
|
lg: 20,
|
|
15
|
+
xl: 28,
|
|
16
|
+
'2xl': 40,
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
interface EmojiAvatarProps {
|
|
@@ -7,14 +7,19 @@ import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'
|
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import { StyleSheet, Text, View } from 'react-native'
|
|
9
9
|
import LinearGradient from 'react-native-linear-gradient'
|
|
10
|
-
import AvatarPrimitive, {
|
|
10
|
+
import AvatarPrimitive, {
|
|
11
|
+
type AvatarRootProps,
|
|
12
|
+
type AvatarSize,
|
|
13
|
+
} from '../primitive/avatar_primitive'
|
|
11
14
|
import { getAvatarGradientProps } from './utils/avatar_gradient_colors'
|
|
12
15
|
|
|
13
|
-
const ICON_SIZE: Record<
|
|
16
|
+
const ICON_SIZE: Record<AvatarSize, number> = {
|
|
14
17
|
xs: 10,
|
|
15
18
|
sm: 12,
|
|
16
19
|
md: 16,
|
|
17
20
|
lg: 20,
|
|
21
|
+
xl: 28,
|
|
22
|
+
'2xl': 40,
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
interface IconAvatarProps {
|
|
@@ -60,7 +60,7 @@ const ANGLE_160: Pick<AvatarGradientProps, 'start' | 'end'> = {
|
|
|
60
60
|
end: { x: 0.35, y: 1 },
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const AVATAR_GRADIENT_MAP: Record<CustomAvatarColorKey, AvatarGradientProps> = {
|
|
63
|
+
export const AVATAR_GRADIENT_MAP: Record<CustomAvatarColorKey, AvatarGradientProps> = {
|
|
64
64
|
'warm-sunset': { colors: ['#F6D06F', '#FB7946', '#B13825'], ...ANGLE_135 },
|
|
65
65
|
peach: { colors: ['#FCD983', '#FC9369'], ...ANGLE_90 },
|
|
66
66
|
'rose-gold': { colors: ['#ED78BE', '#E19084', '#EDB32C'], ...ANGLE_135 },
|
|
@@ -81,7 +81,14 @@ const AVATAR_GRADIENT_MAP: Record<CustomAvatarColorKey, AvatarGradientProps> = {
|
|
|
81
81
|
rainbow: { colors: ['#E76958', '#CCA32C', '#3FA05A', '#8B52D0'], ...ANGLE_135 },
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export const COLOR_KEYS = Object.keys(AVATAR_GRADIENT_MAP) as CustomAvatarColorKey[]
|
|
85
|
+
|
|
86
|
+
export function coerceColorKey(value: string | null | undefined): CustomAvatarColorKey {
|
|
87
|
+
return value && value in AVATAR_GRADIENT_MAP
|
|
88
|
+
? (value as CustomAvatarColorKey)
|
|
89
|
+
: DEFAULT_AVATAR_COLOR_KEY
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
export function getAvatarGradientProps(colorKey?: string | null): AvatarGradientProps {
|
|
85
|
-
|
|
86
|
-
return AVATAR_GRADIENT_MAP[key] || AVATAR_GRADIENT_MAP[DEFAULT_AVATAR_COLOR_KEY]
|
|
93
|
+
return AVATAR_GRADIENT_MAP[coerceColorKey(colorKey)]
|
|
87
94
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useNavigation } from '@react-navigation/native'
|
|
2
2
|
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
|
|
3
|
-
import React, { PropsWithChildren, useEffect, useMemo } from 'react'
|
|
3
|
+
import React, { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'
|
|
4
|
+
import { onAuthRefresh } from '../../utils/auth_events'
|
|
4
5
|
import { ResponseError } from '../../utils/response_error'
|
|
5
6
|
import BlankState from '../primitive/blank_state_primitive'
|
|
6
7
|
|
|
@@ -39,17 +40,16 @@ class ErrorBoundary extends React.Component<PropsWithChildren<{ onReset?: () =>
|
|
|
39
40
|
|
|
40
41
|
function ErrorView({ error, onReset }: { error: Error | ResponseError; onReset: () => void }) {
|
|
41
42
|
const { reset } = useQueryErrorResetBoundary()
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (!reset) return
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
44
|
+
const handleReset = useCallback(() => {
|
|
45
|
+
reset()
|
|
46
|
+
onReset()
|
|
49
47
|
}, [reset, onReset])
|
|
50
48
|
|
|
49
|
+
useEffect(() => handleReset, [handleReset])
|
|
50
|
+
|
|
51
51
|
if (error instanceof ResponseError) {
|
|
52
|
-
return <ResponseErrorView response={error.response} onReset={
|
|
52
|
+
return <ResponseErrorView response={error.response} onReset={handleReset} />
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (isNetworkError(error)) {
|
|
@@ -77,8 +77,9 @@ function isNetworkError(error: ResponseError | Error | TypeError | null) {
|
|
|
77
77
|
return new RegExp(networkFailedMessages.join('|'), 'i').test(error.message)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function ResponseErrorView({ response }: { response: Response; onReset: () => void }) {
|
|
80
|
+
function ResponseErrorView({ response, onReset }: { response: Response; onReset: () => void }) {
|
|
81
81
|
const { status } = response
|
|
82
|
+
|
|
82
83
|
const heading = useMemo(() => {
|
|
83
84
|
switch (status) {
|
|
84
85
|
case 403:
|
|
@@ -101,6 +102,12 @@ function ResponseErrorView({ response }: { response: Response; onReset: () => vo
|
|
|
101
102
|
}
|
|
102
103
|
}, [status])
|
|
103
104
|
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (status !== 401) return
|
|
107
|
+
|
|
108
|
+
return onAuthRefresh(onReset)
|
|
109
|
+
}, [status, onReset])
|
|
110
|
+
|
|
104
111
|
return <ErrorContent heading={heading} body={body} />
|
|
105
112
|
}
|
|
106
113
|
|
|
@@ -38,6 +38,7 @@ export type {
|
|
|
38
38
|
AvatarGroupProps,
|
|
39
39
|
AvatarMaskProps,
|
|
40
40
|
AvatarRootProps,
|
|
41
|
+
AvatarSize,
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// =================================
|
|
@@ -49,6 +50,8 @@ const AVATAR_SIZES = {
|
|
|
49
50
|
sm: 'sm',
|
|
50
51
|
md: 'md',
|
|
51
52
|
lg: 'lg',
|
|
53
|
+
xl: 'xl',
|
|
54
|
+
'2xl': '2xl',
|
|
52
55
|
} as const
|
|
53
56
|
|
|
54
57
|
const AVATAR_PRESENCE_TYPES = {
|
|
@@ -65,6 +68,8 @@ const AVATAR_PX: Record<AvatarSize, number> = {
|
|
|
65
68
|
[AVATAR_SIZES.sm]: 24,
|
|
66
69
|
[AVATAR_SIZES.md]: 32,
|
|
67
70
|
[AVATAR_SIZES.lg]: 40,
|
|
71
|
+
[AVATAR_SIZES.xl]: 56,
|
|
72
|
+
[AVATAR_SIZES['2xl']]: 80,
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
const AVATAR_PRESENCE_PX: Record<AvatarSize, number> = {
|
|
@@ -72,6 +77,8 @@ const AVATAR_PRESENCE_PX: Record<AvatarSize, number> = {
|
|
|
72
77
|
[AVATAR_SIZES.sm]: 10,
|
|
73
78
|
[AVATAR_SIZES.md]: 12,
|
|
74
79
|
[AVATAR_SIZES.lg]: 14,
|
|
80
|
+
[AVATAR_SIZES.xl]: 16,
|
|
81
|
+
[AVATAR_SIZES['2xl']]: 20,
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
const AVATAR_FALLBACK_ICON_PX: Record<AvatarSize, number> = {
|
|
@@ -79,6 +86,8 @@ const AVATAR_FALLBACK_ICON_PX: Record<AvatarSize, number> = {
|
|
|
79
86
|
[AVATAR_SIZES.sm]: 12,
|
|
80
87
|
[AVATAR_SIZES.md]: 16,
|
|
81
88
|
[AVATAR_SIZES.lg]: 20,
|
|
89
|
+
[AVATAR_SIZES.xl]: 28,
|
|
90
|
+
[AVATAR_SIZES['2xl']]: 40,
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
// =================================
|
|
@@ -149,8 +158,8 @@ AvatarRoot.displayName = 'Avatar.Root'
|
|
|
149
158
|
type AvatarMaskProps = ViewProps
|
|
150
159
|
|
|
151
160
|
function AvatarMask({ children, ...props }: AvatarMaskProps) {
|
|
152
|
-
const { maxFontSizeMultiplier, minFontSizeMultiplier } = useAvatarContext()
|
|
153
|
-
const styles = useStyles({ maxFontSizeMultiplier, minFontSizeMultiplier })
|
|
161
|
+
const { size, maxFontSizeMultiplier, minFontSizeMultiplier } = useAvatarContext()
|
|
162
|
+
const styles = useStyles({ size, maxFontSizeMultiplier, minFontSizeMultiplier })
|
|
154
163
|
|
|
155
164
|
return (
|
|
156
165
|
<View style={styles.mask} {...props}>
|
|
@@ -11,6 +11,7 @@ import { ApiClient, useApiClient } from '../hooks/use_api_client'
|
|
|
11
11
|
import { useAppState } from '../hooks/use_app_state'
|
|
12
12
|
import { appGrantsRequestArgs } from '../hooks/use_chat_permissions'
|
|
13
13
|
import { getRequestQueryKey, RequestQueryKey } from '../hooks/use_suspense_api'
|
|
14
|
+
import { emitAuthRefresh } from '../utils/auth_events'
|
|
14
15
|
import { ChatContext, ChatContextValue } from './chat_context'
|
|
15
16
|
|
|
16
17
|
let apiClient: ApiClient | undefined
|
|
@@ -43,6 +44,8 @@ export function ApiProvider({ children }: ViewProps) {
|
|
|
43
44
|
useEffect(() => {
|
|
44
45
|
if (!sessionChanged) return
|
|
45
46
|
|
|
47
|
+
emitAuthRefresh()
|
|
48
|
+
|
|
46
49
|
if (chatQueryClient.isFetching()) {
|
|
47
50
|
chatQueryClient.invalidateQueries()
|
|
48
51
|
return
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Fallback values for use_chat_configuration used when the API request fails.
|
|
2
|
+
// Keeping a static copy here means an API outage doesn't block attachment
|
|
3
|
+
// uploads entirely — users retain the behavior they had before the server
|
|
4
|
+
// was the source of truth.
|
|
5
|
+
//
|
|
6
|
+
// The server is authoritative: if these values drift from the server's
|
|
7
|
+
// ChatConfiguration, the server will still reject uploads that exceed its
|
|
8
|
+
// own rules. These exist only to give the client something reasonable to
|
|
9
|
+
// validate against up-front.
|
|
10
|
+
|
|
11
|
+
export const FALLBACK_ALLOWED_FILE_EXTENSIONS = [
|
|
12
|
+
'.3ga',
|
|
13
|
+
'.3gp',
|
|
14
|
+
'.aac',
|
|
15
|
+
'.amr',
|
|
16
|
+
'.avi',
|
|
17
|
+
'.bmp',
|
|
18
|
+
'.doc',
|
|
19
|
+
'.docx',
|
|
20
|
+
'.gif',
|
|
21
|
+
'.h263',
|
|
22
|
+
'.h264',
|
|
23
|
+
'.heic',
|
|
24
|
+
'.heif',
|
|
25
|
+
'.jpeg',
|
|
26
|
+
'.jpg',
|
|
27
|
+
'.key',
|
|
28
|
+
'.m4a',
|
|
29
|
+
'.m4b',
|
|
30
|
+
'.m4p',
|
|
31
|
+
'.m4r',
|
|
32
|
+
'.m4v',
|
|
33
|
+
'.mkv',
|
|
34
|
+
'.mov',
|
|
35
|
+
'.mp3',
|
|
36
|
+
'.mp4',
|
|
37
|
+
'.mp4-latm',
|
|
38
|
+
'.mpeg',
|
|
39
|
+
'.mpeg4',
|
|
40
|
+
'.mpg',
|
|
41
|
+
'.numbers',
|
|
42
|
+
'.ogg',
|
|
43
|
+
'.pages',
|
|
44
|
+
'.pdf',
|
|
45
|
+
'.png',
|
|
46
|
+
'.ppt',
|
|
47
|
+
'.pptx',
|
|
48
|
+
'.rtf',
|
|
49
|
+
'.txt',
|
|
50
|
+
'.vcf',
|
|
51
|
+
'.wav',
|
|
52
|
+
'.webm',
|
|
53
|
+
'.webp',
|
|
54
|
+
'.wmv',
|
|
55
|
+
'.xls',
|
|
56
|
+
'.xlsx',
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export const FALLBACK_MAX_FILE_SIZE_IN_BYTES = 50 * 1024 * 1024
|
|
60
|
+
|
|
61
|
+
export const FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE = 10
|
|
@@ -12,10 +12,11 @@ interface Props {
|
|
|
12
12
|
|
|
13
13
|
export function useGroupsConversationCreate({ groupId, title, genderId, onSuccess }: Props) {
|
|
14
14
|
const apiClient = useApiClient()
|
|
15
|
+
|
|
15
16
|
return useMutation({
|
|
16
17
|
throwOnError: true,
|
|
17
18
|
onSuccess: result => {
|
|
18
|
-
onSuccess
|
|
19
|
+
onSuccess(result.data.id)
|
|
19
20
|
},
|
|
20
21
|
mutationFn: () =>
|
|
21
22
|
apiClient.groups
|