@planningcenter/chat-react-native 3.32.1-rc.1 → 3.33.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/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/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/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/primitive/avatar_primitive.tsx +11 -2
- 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/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
|
@@ -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
|
}
|
|
@@ -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}>
|
|
@@ -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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMutation, useQuery } from '@tanstack/react-query'
|
|
2
|
-
import {
|
|
2
|
+
import { isNil, omitBy } from 'lodash'
|
|
3
3
|
import { useMemo } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
ApiCollection,
|
|
@@ -14,7 +14,7 @@ import { ApiClient, useApiClient } from '../use_api_client'
|
|
|
14
14
|
interface Props {
|
|
15
15
|
teamIds?: number[]
|
|
16
16
|
planId?: number
|
|
17
|
-
onSuccess?: (conversation: ConversationResource) => void
|
|
17
|
+
onSuccess?: (conversation: ConversationResource, meta: { created: boolean }) => void
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface TeamAndPlanParams {
|
|
@@ -46,8 +46,8 @@ export const useFindOrCreateServicesConversation = ({ teamIds, planId, onSuccess
|
|
|
46
46
|
|
|
47
47
|
const mutation = useMutation({
|
|
48
48
|
throwOnError: true,
|
|
49
|
-
onSuccess:
|
|
50
|
-
onSuccess
|
|
49
|
+
onSuccess: ({ conversation, created }) => {
|
|
50
|
+
onSuccess?.(conversation, { created })
|
|
51
51
|
},
|
|
52
52
|
mutationFn: async () => findOrCreateServicesConversation(apiClient, teamAndPlanParams),
|
|
53
53
|
})
|
|
@@ -58,7 +58,7 @@ export const useFindOrCreateServicesConversation = ({ teamIds, planId, onSuccess
|
|
|
58
58
|
export const findOrCreateServicesConversation = async (
|
|
59
59
|
apiClient: ApiClient,
|
|
60
60
|
teamAndPlanParams: TeamAndPlanParams
|
|
61
|
-
) => {
|
|
61
|
+
): Promise<{ conversation: ConversationResource; created: boolean }> => {
|
|
62
62
|
const foundConversations = await getGroupIdsFromServices(apiClient, teamAndPlanParams)
|
|
63
63
|
.then(res => res.data.groupIdentifiers)
|
|
64
64
|
.then(groupIdentifiers => findConversationWithExactTeams(apiClient, groupIdentifiers))
|
|
@@ -66,13 +66,13 @@ export const findOrCreateServicesConversation = async (
|
|
|
66
66
|
const foundConversation = foundConversations?.data[0]
|
|
67
67
|
|
|
68
68
|
if (foundConversation?.id) {
|
|
69
|
-
return foundConversation
|
|
69
|
+
return { conversation: foundConversation, created: false }
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return fetchServicesPayload(apiClient, teamAndPlanParams)
|
|
73
73
|
.then(res => res.data.payload)
|
|
74
74
|
.then(payload => createConversation(apiClient, payload))
|
|
75
|
-
.then(res => res.data)
|
|
75
|
+
.then(res => ({ conversation: res.data, created: true }))
|
|
76
76
|
.catch(throwResponseError)
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
FileUploadState,
|
|
6
6
|
NativeAttachmentFile,
|
|
7
7
|
} from '../types/resources/denormalized_attachment_resource_for_create'
|
|
8
|
-
import { SUPPORTED_EXTENSIONS } from './attachments/supported_extensions'
|
|
9
8
|
import { useApiClient } from './use_api_client'
|
|
9
|
+
import { useChatConfiguration } from './use_chat_configuration'
|
|
10
10
|
import { useUploadClient } from './use_upload_client'
|
|
11
11
|
|
|
12
12
|
export interface FileError {
|
|
@@ -14,10 +14,6 @@ export interface FileError {
|
|
|
14
14
|
file_size?: boolean
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const MAX_FILE_SIZE_IN_MB = 50
|
|
18
|
-
const MAX_FILE_SIZE_IN_BYTES = MAX_FILE_SIZE_IN_MB * 1024 * 1024
|
|
19
|
-
const MAX_NUMBER_OF_ATTACHMENTS = 10
|
|
20
|
-
|
|
21
17
|
export function useAttachmentUploader({
|
|
22
18
|
conversationId,
|
|
23
19
|
draftAttachments,
|
|
@@ -27,6 +23,9 @@ export function useAttachmentUploader({
|
|
|
27
23
|
}) {
|
|
28
24
|
const apiClient = useApiClient()
|
|
29
25
|
const uploadApi = useUploadClient()
|
|
26
|
+
const { allowedFileExtensions, maxFileSizeInBytes, maxAttachmentsPerMessage } =
|
|
27
|
+
useChatConfiguration()
|
|
28
|
+
const maxFileSizeInMb = Number((maxFileSizeInBytes / (1024 * 1024)).toFixed(1))
|
|
30
29
|
const [attachments, setAttachments] = useState<FileAttachment[]>(() => draftAttachments || [])
|
|
31
30
|
const uploadState = useRef<FileUploadState>({})
|
|
32
31
|
const [lastUploadId, setLastUploadId] = useState<string>()
|
|
@@ -38,9 +37,9 @@ export function useAttachmentUploader({
|
|
|
38
37
|
const fileErrors = {} as FileError
|
|
39
38
|
|
|
40
39
|
const validFiles = files.filter(file => {
|
|
41
|
-
const extension = file.name.split('.').pop() as string
|
|
42
|
-
const isValidExtension =
|
|
43
|
-
const isValidFileSize = file.size <=
|
|
40
|
+
const extension = file.name.toLowerCase().split('.').pop() as string
|
|
41
|
+
const isValidExtension = allowedFileExtensions.includes(`.${extension}`)
|
|
42
|
+
const isValidFileSize = file.size <= maxFileSizeInBytes
|
|
44
43
|
|
|
45
44
|
if (!isValidExtension) {
|
|
46
45
|
fileErrors.file_type ||= []
|
|
@@ -60,10 +59,10 @@ export function useAttachmentUploader({
|
|
|
60
59
|
)
|
|
61
60
|
}
|
|
62
61
|
if (fileErrors.file_size) {
|
|
63
|
-
errorMessages.push(`File size exceeds ${
|
|
62
|
+
errorMessages.push(`File size exceeds ${maxFileSizeInMb} MB`)
|
|
64
63
|
}
|
|
65
|
-
if (numberOfAttachments + validFiles.length >
|
|
66
|
-
errorMessages.push(`You can't attach more than ${
|
|
64
|
+
if (numberOfAttachments + validFiles.length > maxAttachmentsPerMessage) {
|
|
65
|
+
errorMessages.push(`You can't attach more than ${maxAttachmentsPerMessage} files at once.`)
|
|
67
66
|
}
|
|
68
67
|
if (errorMessages.length > 0) {
|
|
69
68
|
setErrorMessage(errorMessages.join('\n'))
|
|
@@ -100,21 +99,34 @@ export function useAttachmentUploader({
|
|
|
100
99
|
}
|
|
101
100
|
setLastUploadId(messageAttachmentId)
|
|
102
101
|
})
|
|
103
|
-
.catch(err => {
|
|
102
|
+
.catch(async err => {
|
|
104
103
|
const isFlagged = err?.code === 'image_flagged'
|
|
105
104
|
uploadState.current[attachment.file.name] = {
|
|
106
105
|
status: 'error',
|
|
107
106
|
flagged: isFlagged,
|
|
108
107
|
}
|
|
109
108
|
if (!isFlagged) {
|
|
110
|
-
|
|
109
|
+
// Dev builds surface the raw server detail to shorten the
|
|
110
|
+
// feedback loop on backend errors (e.g. AWS SSO re-auth).
|
|
111
|
+
// Production users always see the generic message.
|
|
112
|
+
const serverDetail = __DEV__ ? await extractDevOnlyServerErrorDetail(err) : null
|
|
113
|
+
setErrorMessage(serverDetail ?? 'This file could not be uploaded.')
|
|
111
114
|
}
|
|
112
115
|
setLastUploadId(attachment.file.name)
|
|
113
116
|
})
|
|
114
117
|
})
|
|
115
118
|
}
|
|
116
119
|
},
|
|
117
|
-
[
|
|
120
|
+
[
|
|
121
|
+
numberOfAttachments,
|
|
122
|
+
uploadApi,
|
|
123
|
+
apiClient.chat,
|
|
124
|
+
conversationId,
|
|
125
|
+
allowedFileExtensions,
|
|
126
|
+
maxFileSizeInBytes,
|
|
127
|
+
maxFileSizeInMb,
|
|
128
|
+
maxAttachmentsPerMessage,
|
|
129
|
+
]
|
|
118
130
|
)
|
|
119
131
|
|
|
120
132
|
useEffect(() => {
|
|
@@ -168,7 +180,19 @@ export function useAttachmentUploader({
|
|
|
168
180
|
pendingUploads,
|
|
169
181
|
errorMessage,
|
|
170
182
|
flaggedAttachmentCount,
|
|
171
|
-
remainingAttachable:
|
|
183
|
+
remainingAttachable: Math.max(0, maxAttachmentsPerMessage - numberOfAttachments),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function extractDevOnlyServerErrorDetail(err: unknown): Promise<string | null> {
|
|
188
|
+
const response = err as Response | null
|
|
189
|
+
if (!response?.clone) return null
|
|
190
|
+
try {
|
|
191
|
+
const body = await response.clone().json()
|
|
192
|
+
const detail = body?.errors?.[0]?.detail ?? body?.detail
|
|
193
|
+
return typeof detail === 'string' ? detail : null
|
|
194
|
+
} catch {
|
|
195
|
+
return null
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
198
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
2
|
+
import { ApiResource } from '../types'
|
|
3
|
+
import type { ChatConfigurationResource } from '../types/resources/chat_configuration_resource'
|
|
4
|
+
import {
|
|
5
|
+
getChatConfigurationRequestArgs,
|
|
6
|
+
getChatConfigurationQueryKey,
|
|
7
|
+
} from '../utils/request/get_chat_configuration'
|
|
8
|
+
import {
|
|
9
|
+
FALLBACK_ALLOWED_FILE_EXTENSIONS,
|
|
10
|
+
FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
|
|
11
|
+
FALLBACK_MAX_FILE_SIZE_IN_BYTES,
|
|
12
|
+
} from './attachments/fallback_chat_configuration'
|
|
13
|
+
import { useApiClient } from './use_api_client'
|
|
14
|
+
|
|
15
|
+
// Client-side mirror of ChatConfiguration (server is source of truth).
|
|
16
|
+
// Returns fallback values if the API request fails so that the attachment
|
|
17
|
+
// UX keeps working during transient server issues. The server still
|
|
18
|
+
// enforces its own limits — this hook only drives pre-upload UX.
|
|
19
|
+
export function useChatConfiguration() {
|
|
20
|
+
const apiClient = useApiClient()
|
|
21
|
+
const requestArgs = getChatConfigurationRequestArgs()
|
|
22
|
+
|
|
23
|
+
const { data } = useSuspenseQuery({
|
|
24
|
+
queryKey: getChatConfigurationQueryKey(),
|
|
25
|
+
queryFn: () => {
|
|
26
|
+
return apiClient.chat
|
|
27
|
+
.get<ApiResource<ChatConfigurationResource>>(requestArgs)
|
|
28
|
+
.catch(() => stableFallbackConfiguration)
|
|
29
|
+
},
|
|
30
|
+
staleTime: 1000 * 60 * 60, // 1 hour — this rarely changes
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const attrs = data.data
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
allowedFileExtensions: attrs.allowedFileExtensions,
|
|
37
|
+
maxFileSizeInBytes: attrs.maxFileSizeInBytes,
|
|
38
|
+
maxAttachmentsPerMessage: attrs.maxAttachmentsPerMessage,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Shape matches what consumers see after transform_response flattens
|
|
43
|
+
// attributes and camelCases keys.
|
|
44
|
+
const stableFallbackConfiguration: ApiResource<ChatConfigurationResource> = {
|
|
45
|
+
data: {
|
|
46
|
+
type: 'ChatConfiguration',
|
|
47
|
+
id: 'current',
|
|
48
|
+
allowedFileExtensions: FALLBACK_ALLOWED_FILE_EXTENSIONS,
|
|
49
|
+
maxFileSizeInBytes: FALLBACK_MAX_FILE_SIZE_IN_BYTES,
|
|
50
|
+
maxAttachmentsPerMessage: FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
|
|
51
|
+
},
|
|
52
|
+
links: {},
|
|
53
|
+
meta: {},
|
|
54
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { Alert } from 'react-native'
|
|
3
|
+
import type { ConversationResource } from '../types'
|
|
4
|
+
import type { ApiResource } from '../types'
|
|
5
|
+
import { transformGetToPost } from '../utils/client/request_helpers'
|
|
6
|
+
import type { ImagePickerAsset } from '../utils/native_adapters/image_picker'
|
|
7
|
+
import { ApiClient, useApiClient } from './use_api_client'
|
|
8
|
+
import { getConversationRequestArgs } from './use_conversation'
|
|
9
|
+
import { getRequestQueryKey } from './use_suspense_api'
|
|
10
|
+
import { useUploadClient } from './use_upload_client'
|
|
11
|
+
|
|
12
|
+
export type AvatarType = 'icon' | 'emoji' | 'image'
|
|
13
|
+
|
|
14
|
+
export type AvatarUpdatePayload =
|
|
15
|
+
| { kind: Extract<AvatarType, 'icon' | 'emoji'>; key: string; color: string }
|
|
16
|
+
| { kind: Extract<AvatarType, 'image'>; imageAsset: ImagePickerAsset; color: string }
|
|
17
|
+
| { kind: 'clear' }
|
|
18
|
+
|
|
19
|
+
export const useConversationAvatarUpdate = ({ conversationId }: { conversationId: number }) => {
|
|
20
|
+
const apiClient = useApiClient()
|
|
21
|
+
const uploadClient = useUploadClient()
|
|
22
|
+
const queryClient = useQueryClient()
|
|
23
|
+
const requestArgs = getConversationRequestArgs({ conversation_id: conversationId })
|
|
24
|
+
const queryKey = getRequestQueryKey(requestArgs)
|
|
25
|
+
|
|
26
|
+
return useMutation({
|
|
27
|
+
mutationKey: ['updateConversationAvatar', conversationId],
|
|
28
|
+
mutationFn: async (payload: AvatarUpdatePayload) => {
|
|
29
|
+
const postArgs = transformGetToPost(requestArgs).data
|
|
30
|
+
const attributes = await buildAttributes(payload, uploadClient)
|
|
31
|
+
|
|
32
|
+
return apiClient.chat.patch<ApiResource<ConversationResource>>({
|
|
33
|
+
url: `/me/conversations/${conversationId}/`,
|
|
34
|
+
data: { data: { type: '', attributes }, ...postArgs },
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
onMutate: async (payload: AvatarUpdatePayload) => {
|
|
38
|
+
await queryClient.cancelQueries({ queryKey })
|
|
39
|
+
|
|
40
|
+
const previous = queryClient.getQueryData<ApiResource<ConversationResource>>(queryKey)
|
|
41
|
+
|
|
42
|
+
queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {
|
|
43
|
+
if (!prev?.data) return prev
|
|
44
|
+
return { ...prev, data: buildOptimisticData(prev.data, payload) }
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return { previous }
|
|
48
|
+
},
|
|
49
|
+
onSuccess: (response, _payload, _context) => {
|
|
50
|
+
queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, () => response)
|
|
51
|
+
queryClient.invalidateQueries({ queryKey: ['/me/conversations'] })
|
|
52
|
+
},
|
|
53
|
+
onError: (_error, _payload, context) => {
|
|
54
|
+
if (context?.previous) {
|
|
55
|
+
queryClient.setQueryData(queryKey, context.previous)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Alert.alert('Error', 'Failed to update conversation avatar. Please try again.')
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function patchConversationAvatar(
|
|
64
|
+
apiClient: ApiClient,
|
|
65
|
+
uploadClient: ReturnType<typeof useUploadClient>,
|
|
66
|
+
conversationId: number,
|
|
67
|
+
payload: Exclude<AvatarUpdatePayload, { kind: 'clear' }>
|
|
68
|
+
) {
|
|
69
|
+
const requestArgs = getConversationRequestArgs({ conversation_id: conversationId })
|
|
70
|
+
const postArgs = transformGetToPost(requestArgs).data
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const attributes = await buildAttributes(payload, uploadClient)
|
|
74
|
+
await apiClient.chat.patch({
|
|
75
|
+
url: `/me/conversations/${conversationId}/`,
|
|
76
|
+
data: { data: { type: '', attributes }, ...postArgs },
|
|
77
|
+
})
|
|
78
|
+
} catch {
|
|
79
|
+
Alert.alert(
|
|
80
|
+
'Avatar not saved',
|
|
81
|
+
'The conversation was created, but the avatar could not be saved. You can update it from the conversation details.'
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildOptimisticData(
|
|
87
|
+
data: ConversationResource,
|
|
88
|
+
payload: AvatarUpdatePayload
|
|
89
|
+
): ConversationResource {
|
|
90
|
+
switch (payload.kind) {
|
|
91
|
+
case 'icon':
|
|
92
|
+
return {
|
|
93
|
+
...data,
|
|
94
|
+
customAvatarType: 'icon',
|
|
95
|
+
customAvatarKey: payload.key,
|
|
96
|
+
customAvatarColor: payload.color,
|
|
97
|
+
customAvatarImageUrl: null,
|
|
98
|
+
}
|
|
99
|
+
case 'emoji':
|
|
100
|
+
return {
|
|
101
|
+
...data,
|
|
102
|
+
customAvatarType: 'emoji',
|
|
103
|
+
customAvatarKey: payload.key,
|
|
104
|
+
customAvatarColor: payload.color,
|
|
105
|
+
customAvatarImageUrl: null,
|
|
106
|
+
}
|
|
107
|
+
case 'image':
|
|
108
|
+
return {
|
|
109
|
+
...data,
|
|
110
|
+
customAvatarType: 'image',
|
|
111
|
+
customAvatarKey: null,
|
|
112
|
+
customAvatarColor: payload.color,
|
|
113
|
+
customAvatarImageUrl: payload.imageAsset.uri,
|
|
114
|
+
}
|
|
115
|
+
case 'clear':
|
|
116
|
+
return {
|
|
117
|
+
...data,
|
|
118
|
+
customAvatarType: null,
|
|
119
|
+
customAvatarKey: null,
|
|
120
|
+
customAvatarColor: null,
|
|
121
|
+
customAvatarImageUrl: null,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function buildAttributes(
|
|
127
|
+
payload: AvatarUpdatePayload,
|
|
128
|
+
uploadClient: ReturnType<typeof useUploadClient>
|
|
129
|
+
) {
|
|
130
|
+
switch (payload.kind) {
|
|
131
|
+
case 'icon':
|
|
132
|
+
return {
|
|
133
|
+
custom_avatar_type: 'icon',
|
|
134
|
+
custom_avatar_key: payload.key,
|
|
135
|
+
custom_avatar_color: payload.color,
|
|
136
|
+
}
|
|
137
|
+
case 'emoji':
|
|
138
|
+
return {
|
|
139
|
+
custom_avatar_type: 'emoji',
|
|
140
|
+
custom_avatar_key: payload.key,
|
|
141
|
+
custom_avatar_color: payload.color,
|
|
142
|
+
}
|
|
143
|
+
case 'image': {
|
|
144
|
+
const uploaded = await uploadClient.uploadFile({
|
|
145
|
+
uri: payload.imageAsset.uri,
|
|
146
|
+
name: payload.imageAsset.fileName || 'avatar.jpg',
|
|
147
|
+
type: payload.imageAsset.mimeType || 'image/jpeg',
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
custom_avatar_type: 'image',
|
|
152
|
+
custom_avatar_color: payload.color,
|
|
153
|
+
avatar_uploaded_file_id: uploaded.id,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
case 'clear':
|
|
157
|
+
return {
|
|
158
|
+
custom_avatar_type: null,
|
|
159
|
+
custom_avatar_key: null,
|
|
160
|
+
custom_avatar_color: null,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -37,6 +37,7 @@ export const availableFeatures = {
|
|
|
37
37
|
gender_specific_conversations: 'ROLLOUT_gender_specific_conversations',
|
|
38
38
|
message_reporting: 'ROLLOUT_MOBILE_message_reporting',
|
|
39
39
|
granular_notifications_ui: 'ROLLOUT_granular_notification_preferences_ui',
|
|
40
|
+
custom_conversation_avatars: 'ROLLOUT_custom_conversation_avatars',
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const stableEmptyFeatures: ApiCollection<FeatureResource> = {
|
package/src/navigation/index.tsx
CHANGED
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
AttachmentActionsScreen,
|
|
18
18
|
AttachmentActionsScreenOptions,
|
|
19
19
|
} from '../screens/attachment_actions/attachment_actions_screen'
|
|
20
|
+
import {
|
|
21
|
+
AvatarPickerScreen,
|
|
22
|
+
AvatarPickerScreenOptions,
|
|
23
|
+
AvatarPickerCreateScreenOptions,
|
|
24
|
+
} from '../screens/avatar_picker/avatar_picker_screen'
|
|
20
25
|
import { BugReportScreen, BugReportScreenOptions } from '../screens/bug_report_screen'
|
|
21
26
|
import {
|
|
22
27
|
MessageReadReceiptsScreen,
|
|
@@ -148,6 +153,10 @@ export const NewConversationStack = createNativeStackNavigator({
|
|
|
148
153
|
),
|
|
149
154
|
}),
|
|
150
155
|
},
|
|
156
|
+
AvatarPicker: {
|
|
157
|
+
screen: AvatarPickerScreen,
|
|
158
|
+
options: AvatarPickerCreateScreenOptions,
|
|
159
|
+
},
|
|
151
160
|
},
|
|
152
161
|
})
|
|
153
162
|
|
|
@@ -252,6 +261,10 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
252
261
|
),
|
|
253
262
|
}),
|
|
254
263
|
},
|
|
264
|
+
AvatarPicker: {
|
|
265
|
+
screen: AvatarPickerScreen,
|
|
266
|
+
options: AvatarPickerScreenOptions,
|
|
267
|
+
},
|
|
255
268
|
NotificationSettings: {
|
|
256
269
|
screen: NotificationSettingsScreen,
|
|
257
270
|
options: ({ navigation }) => ({
|