@planningcenter/chat-react-native 3.34.0-rc.4 → 3.34.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/attachments/generic_file_attachment.d.ts +0 -1
- package/build/components/conversation/attachments/generic_file_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/generic_file_attachment.js +1 -16
- package/build/components/conversation/attachments/generic_file_attachment.js.map +1 -1
- package/build/components/conversation/message_form/message_form_attachment_file.d.ts +11 -0
- package/build/components/conversation/message_form/message_form_attachment_file.d.ts.map +1 -0
- package/build/components/conversation/message_form/message_form_attachment_file.js +8 -0
- package/build/components/conversation/message_form/message_form_attachment_file.js.map +1 -0
- package/build/components/conversation/message_form/message_form_attachment_video.d.ts.map +1 -1
- package/build/components/conversation/message_form/message_form_attachment_video.js +1 -2
- package/build/components/conversation/message_form/message_form_attachment_video.js.map +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +13 -8
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/display/file_attachment_preview.d.ts +11 -0
- package/build/components/display/file_attachment_preview.d.ts.map +1 -0
- package/build/components/display/file_attachment_preview.js +95 -0
- package/build/components/display/file_attachment_preview.js.map +1 -0
- package/build/components/display/image.d.ts.map +1 -1
- package/build/components/display/image.js +4 -1
- package/build/components/display/image.js.map +1 -1
- package/build/components/display/index.d.ts +1 -0
- package/build/components/display/index.d.ts.map +1 -1
- package/build/components/display/index.js +1 -0
- package/build/components/display/index.js.map +1 -1
- package/build/hooks/attachments/fallback_chat_configuration.d.ts +1 -0
- package/build/hooks/attachments/fallback_chat_configuration.d.ts.map +1 -1
- package/build/hooks/attachments/fallback_chat_configuration.js +23 -0
- package/build/hooks/attachments/fallback_chat_configuration.js.map +1 -1
- package/build/hooks/use_chat_configuration.d.ts +1 -0
- package/build/hooks/use_chat_configuration.d.ts.map +1 -1
- package/build/hooks/use_chat_configuration.js +9 -1
- package/build/hooks/use_chat_configuration.js.map +1 -1
- package/build/types/resources/chat_configuration_resource.d.ts +1 -0
- package/build/types/resources/chat_configuration_resource.d.ts.map +1 -1
- package/build/types/resources/chat_configuration_resource.js.map +1 -1
- package/build/utils/attachment_kind.d.ts +5 -0
- package/build/utils/attachment_kind.d.ts.map +1 -0
- package/build/utils/attachment_kind.js +46 -0
- package/build/utils/attachment_kind.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/native_adapters/document_picker.d.ts +5 -2
- package/build/utils/native_adapters/document_picker.d.ts.map +1 -1
- package/build/utils/native_adapters/document_picker.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/hooks/use_chat_configuration.test.tsx +26 -0
- package/src/components/conversation/attachments/generic_file_attachment.tsx +1 -14
- package/src/components/conversation/message_form/message_form_attachment_file.tsx +26 -0
- package/src/components/conversation/message_form/message_form_attachment_video.tsx +1 -2
- package/src/components/conversation/message_form.tsx +23 -8
- package/src/components/display/file_attachment_preview.tsx +135 -0
- package/src/components/display/image.tsx +5 -0
- package/src/components/display/index.ts +1 -0
- package/src/hooks/attachments/fallback_chat_configuration.ts +24 -0
- package/src/hooks/use_chat_configuration.ts +9 -0
- package/src/types/resources/chat_configuration_resource.ts +4 -0
- package/src/utils/__tests__/attachment_kind.test.ts +37 -0
- package/src/utils/attachment_kind.ts +47 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/native_adapters/document_picker.ts +9 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { View, StyleSheet } from 'react-native'
|
|
2
|
+
import { useTheme } from '../../hooks'
|
|
3
|
+
import { platformFontWeightMedium } from '../../utils'
|
|
4
|
+
import { getAttachmentIconName } from '../../utils/attachment_kind'
|
|
5
|
+
import { tokens } from '../../vendor/tapestry/tokens'
|
|
6
|
+
import { Icon } from './icon'
|
|
7
|
+
import { IconButton } from './icon_button'
|
|
8
|
+
import { Spinner } from './spinner'
|
|
9
|
+
import { Text } from './text'
|
|
10
|
+
|
|
11
|
+
interface FileAttachmentPreviewProps {
|
|
12
|
+
name: string
|
|
13
|
+
contentType?: string
|
|
14
|
+
onRemovePress: () => void
|
|
15
|
+
loading?: boolean
|
|
16
|
+
error?: boolean
|
|
17
|
+
hideRemoveButton?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const FileAttachmentPreview = ({
|
|
21
|
+
name,
|
|
22
|
+
contentType,
|
|
23
|
+
onRemovePress,
|
|
24
|
+
loading = false,
|
|
25
|
+
error = false,
|
|
26
|
+
hideRemoveButton = false,
|
|
27
|
+
}: FileAttachmentPreviewProps) => {
|
|
28
|
+
const styles = useStyles({ error })
|
|
29
|
+
|
|
30
|
+
if (loading) {
|
|
31
|
+
return (
|
|
32
|
+
<View style={styles.container}>
|
|
33
|
+
<Spinner size={20} style={styles.spinner} />
|
|
34
|
+
</View>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={styles.container}>
|
|
40
|
+
<View style={styles.contentContainer}>
|
|
41
|
+
<Icon name={getAttachmentIconName(contentType)} size={18} style={styles.fileIcon} />
|
|
42
|
+
<View style={styles.textContainer}>
|
|
43
|
+
<Text
|
|
44
|
+
variant="tertiary"
|
|
45
|
+
numberOfLines={1}
|
|
46
|
+
style={styles.nameText}
|
|
47
|
+
accessibilityLabel={`File attachment: ${name}`}
|
|
48
|
+
>
|
|
49
|
+
{name}
|
|
50
|
+
</Text>
|
|
51
|
+
</View>
|
|
52
|
+
</View>
|
|
53
|
+
{!hideRemoveButton && (
|
|
54
|
+
<IconButton
|
|
55
|
+
name="general.x"
|
|
56
|
+
onPress={onRemovePress}
|
|
57
|
+
size="xxs"
|
|
58
|
+
appearance="neutral"
|
|
59
|
+
style={styles.closeButton}
|
|
60
|
+
accessibilityLabel="Remove file attachment"
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
{error && (
|
|
64
|
+
<View style={styles.errorBadge}>
|
|
65
|
+
<Icon name="general.exclamationTriangle" size={12} style={styles.errorIcon} />
|
|
66
|
+
</View>
|
|
67
|
+
)}
|
|
68
|
+
</View>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const useStyles = ({ error }: Partial<FileAttachmentPreviewProps>) => {
|
|
73
|
+
const { colors } = useTheme()
|
|
74
|
+
const borderRadius = 8
|
|
75
|
+
|
|
76
|
+
return StyleSheet.create({
|
|
77
|
+
container: {
|
|
78
|
+
height: 60,
|
|
79
|
+
minWidth: 150,
|
|
80
|
+
flexDirection: 'row',
|
|
81
|
+
justifyContent: 'space-between',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
gap: 8,
|
|
84
|
+
backgroundColor: colors.fillColorNeutral070,
|
|
85
|
+
borderColor: error ? colors.statusErrorBorder : colors.borderColorDefaultBase,
|
|
86
|
+
borderWidth: error ? 2 : 1,
|
|
87
|
+
borderRadius,
|
|
88
|
+
padding: 4,
|
|
89
|
+
},
|
|
90
|
+
contentContainer: {
|
|
91
|
+
flexDirection: 'row',
|
|
92
|
+
gap: 8,
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
flexShrink: 1,
|
|
95
|
+
paddingHorizontal: 8,
|
|
96
|
+
paddingVertical: 4,
|
|
97
|
+
},
|
|
98
|
+
textContainer: {
|
|
99
|
+
flexShrink: 1,
|
|
100
|
+
flexDirection: 'column',
|
|
101
|
+
},
|
|
102
|
+
fileIcon: {
|
|
103
|
+
color: error ? colors.iconColorDefaultDisabled : colors.iconColorDefaultPrimary,
|
|
104
|
+
},
|
|
105
|
+
nameText: {
|
|
106
|
+
color: error ? colors.textColorDefaultDisabled : colors.textColorDefaultPrimary,
|
|
107
|
+
fontWeight: platformFontWeightMedium,
|
|
108
|
+
flexShrink: 1,
|
|
109
|
+
},
|
|
110
|
+
closeButton: {
|
|
111
|
+
backgroundColor: colors.fillColorNeutral050Base,
|
|
112
|
+
borderRadius: 16,
|
|
113
|
+
height: 20,
|
|
114
|
+
width: 20,
|
|
115
|
+
alignSelf: 'flex-start',
|
|
116
|
+
},
|
|
117
|
+
errorBadge: {
|
|
118
|
+
backgroundColor: colors.statusErrorBorder,
|
|
119
|
+
position: 'absolute',
|
|
120
|
+
bottom: 0,
|
|
121
|
+
right: 0,
|
|
122
|
+
zIndex: 2,
|
|
123
|
+
borderStartStartRadius: borderRadius,
|
|
124
|
+
padding: 4,
|
|
125
|
+
},
|
|
126
|
+
errorIcon: {
|
|
127
|
+
color: tokens.colorNeutral100White,
|
|
128
|
+
transform: [{ translateX: 1 }],
|
|
129
|
+
fontSize: 10,
|
|
130
|
+
},
|
|
131
|
+
spinner: {
|
|
132
|
+
marginHorizontal: 'auto',
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|
|
@@ -79,6 +79,10 @@ export function Image({
|
|
|
79
79
|
onLoad?.(event)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
const handleOnError = () => {
|
|
83
|
+
setIsImageLoading(false)
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
const isLoading = isImageLoading || loading
|
|
83
87
|
|
|
84
88
|
const ImageComponent = animatedImageStyle ? Animated.Image : ReactNativeImage
|
|
@@ -94,6 +98,7 @@ export function Image({
|
|
|
94
98
|
<ImageComponent
|
|
95
99
|
style={[styles.image, imageStyles, animatedImageStyle]}
|
|
96
100
|
onLoad={handleOnLoad}
|
|
101
|
+
onError={handleOnError}
|
|
97
102
|
source={source}
|
|
98
103
|
alt={isLoading ? '' : alt}
|
|
99
104
|
{...props}
|
|
@@ -12,6 +12,7 @@ export * from './heading'
|
|
|
12
12
|
export * from './icon_button'
|
|
13
13
|
export * from './icon'
|
|
14
14
|
export * from './image'
|
|
15
|
+
export * from './file_attachment_preview'
|
|
15
16
|
export * from './image_attachment_preview'
|
|
16
17
|
export * from './video_attachment_preview'
|
|
17
18
|
export * from './person'
|
|
@@ -56,6 +56,30 @@ export const FALLBACK_ALLOWED_FILE_EXTENSIONS = [
|
|
|
56
56
|
'.xlsx',
|
|
57
57
|
]
|
|
58
58
|
|
|
59
|
+
// Broad MIME categories covering the extensions in
|
|
60
|
+
// FALLBACK_ALLOWED_FILE_EXTENSIONS. Used to constrain native pickers up
|
|
61
|
+
// front; final accept/reject still uses the extension list because the
|
|
62
|
+
// picker's `type` filter is advisory on iOS and Android.
|
|
63
|
+
export const FALLBACK_ALLOWED_MIME_TYPES = [
|
|
64
|
+
'image/*',
|
|
65
|
+
'video/*',
|
|
66
|
+
'audio/*',
|
|
67
|
+
'application/pdf',
|
|
68
|
+
'application/msword',
|
|
69
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
70
|
+
'application/vnd.ms-excel',
|
|
71
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
72
|
+
'application/vnd.ms-powerpoint',
|
|
73
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
74
|
+
'application/vnd.apple.pages',
|
|
75
|
+
'application/vnd.apple.numbers',
|
|
76
|
+
'application/vnd.apple.keynote',
|
|
77
|
+
'application/rtf',
|
|
78
|
+
'text/plain',
|
|
79
|
+
'text/rtf',
|
|
80
|
+
'text/vcard',
|
|
81
|
+
]
|
|
82
|
+
|
|
59
83
|
export const FALLBACK_MAX_FILE_SIZE_IN_BYTES = 50 * 1024 * 1024
|
|
60
84
|
|
|
61
85
|
export const FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE = 10
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '../utils/request/get_chat_configuration'
|
|
8
8
|
import {
|
|
9
9
|
FALLBACK_ALLOWED_FILE_EXTENSIONS,
|
|
10
|
+
FALLBACK_ALLOWED_MIME_TYPES,
|
|
10
11
|
FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
|
|
11
12
|
FALLBACK_MAX_FILE_SIZE_IN_BYTES,
|
|
12
13
|
} from './attachments/fallback_chat_configuration'
|
|
@@ -34,6 +35,13 @@ export function useChatConfiguration() {
|
|
|
34
35
|
|
|
35
36
|
return {
|
|
36
37
|
allowedFileExtensions: attrs.allowedFileExtensions,
|
|
38
|
+
// During the server-side rollout the API may omit allowedMimeTypes.
|
|
39
|
+
// The static fallback covers the same set of file kinds as the server's
|
|
40
|
+
// hardcoded ALLOWED_FILE_EXTENSIONS, so the picker filter and the
|
|
41
|
+
// post-pick extension validator agree. Once the server always returns
|
|
42
|
+
// allowedMimeTypes, the resource type can tighten from optional to
|
|
43
|
+
// required and this nullish-coalesce becomes pure defense-in-depth.
|
|
44
|
+
allowedMimeTypes: attrs.allowedMimeTypes ?? FALLBACK_ALLOWED_MIME_TYPES,
|
|
37
45
|
maxFileSizeInBytes: attrs.maxFileSizeInBytes,
|
|
38
46
|
maxAttachmentsPerMessage: attrs.maxAttachmentsPerMessage,
|
|
39
47
|
}
|
|
@@ -46,6 +54,7 @@ const stableFallbackConfiguration: ApiResource<ChatConfigurationResource> = {
|
|
|
46
54
|
type: 'ChatConfiguration',
|
|
47
55
|
id: 'current',
|
|
48
56
|
allowedFileExtensions: FALLBACK_ALLOWED_FILE_EXTENSIONS,
|
|
57
|
+
allowedMimeTypes: FALLBACK_ALLOWED_MIME_TYPES,
|
|
49
58
|
maxFileSizeInBytes: FALLBACK_MAX_FILE_SIZE_IN_BYTES,
|
|
50
59
|
maxAttachmentsPerMessage: FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
|
|
51
60
|
},
|
|
@@ -6,6 +6,10 @@ export interface ChatConfigurationResource {
|
|
|
6
6
|
type: 'ChatConfiguration'
|
|
7
7
|
id: string
|
|
8
8
|
allowedFileExtensions: string[]
|
|
9
|
+
// Optional during server-side rollout. Once the server always returns it,
|
|
10
|
+
// tighten to `string[]`. Until then, useChatConfiguration falls back to
|
|
11
|
+
// FALLBACK_ALLOWED_MIME_TYPES.
|
|
12
|
+
allowedMimeTypes?: string[]
|
|
9
13
|
maxFileSizeInBytes: number
|
|
10
14
|
maxAttachmentsPerMessage: number
|
|
11
15
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { pickAttachmentPreviewKind } from '../attachment_kind'
|
|
2
|
+
|
|
3
|
+
describe('pickAttachmentPreviewKind', () => {
|
|
4
|
+
describe('with a usable MIME type', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['image/png', 'image'],
|
|
7
|
+
['image/jpeg', 'image'],
|
|
8
|
+
['video/mp4', 'video'],
|
|
9
|
+
['video/quicktime', 'video'],
|
|
10
|
+
['application/pdf', 'file'],
|
|
11
|
+
['text/plain', 'file'],
|
|
12
|
+
['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'file'],
|
|
13
|
+
['', 'file'],
|
|
14
|
+
[undefined, 'file'],
|
|
15
|
+
])('classifies %p as %p', (input, expected) => {
|
|
16
|
+
expect(pickAttachmentPreviewKind(input)).toBe(expected)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Document picker providers sometimes drop the MIME type. Without an
|
|
21
|
+
// extension-based fallback, octet-stream-typed images and videos would
|
|
22
|
+
// render as generic file tiles even though the bytes are decodable.
|
|
23
|
+
describe('with an unhelpful MIME type', () => {
|
|
24
|
+
it.each([
|
|
25
|
+
['application/octet-stream', 'IMG_1234.HEIC', 'image'],
|
|
26
|
+
['application/octet-stream', 'photo.jpg', 'image'],
|
|
27
|
+
['application/octet-stream', 'clip.mov', 'video'],
|
|
28
|
+
['application/octet-stream', 'movie.MP4', 'video'],
|
|
29
|
+
['application/octet-stream', 'spreadsheet.xlsx', 'file'],
|
|
30
|
+
[undefined, 'photo.png', 'image'],
|
|
31
|
+
['application/octet-stream', undefined, 'file'],
|
|
32
|
+
['application/octet-stream', 'no_extension', 'file'],
|
|
33
|
+
])('with type %p and name %p classifies as %p', (type, name, expected) => {
|
|
34
|
+
expect(pickAttachmentPreviewKind(type, name)).toBe(expected)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IconString } from '../components/display/icon'
|
|
2
|
+
|
|
3
|
+
export type AttachmentPreviewKind = 'image' | 'video' | 'file'
|
|
4
|
+
|
|
5
|
+
// Document picker providers sometimes hand back assets without a usable
|
|
6
|
+
// MIME type (Android `MediaStore` content URIs, some cloud-storage
|
|
7
|
+
// providers). The asset's `type` then arrives as `application/octet-stream`
|
|
8
|
+
// or undefined even though the file is genuinely an image or video. Fall
|
|
9
|
+
// back to extension-based classification in that case so the preview
|
|
10
|
+
// matches the actual content.
|
|
11
|
+
const IMAGE_EXTENSIONS = new Set(['bmp', 'gif', 'heic', 'heif', 'jpeg', 'jpg', 'png', 'webp'])
|
|
12
|
+
|
|
13
|
+
const VIDEO_EXTENSIONS = new Set([
|
|
14
|
+
'3gp',
|
|
15
|
+
'avi',
|
|
16
|
+
'h263',
|
|
17
|
+
'h264',
|
|
18
|
+
'm4v',
|
|
19
|
+
'mkv',
|
|
20
|
+
'mov',
|
|
21
|
+
'mp4',
|
|
22
|
+
'mpeg',
|
|
23
|
+
'mpeg4',
|
|
24
|
+
'mpg',
|
|
25
|
+
'webm',
|
|
26
|
+
'wmv',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
export function pickAttachmentPreviewKind(
|
|
30
|
+
type: string | undefined,
|
|
31
|
+
name?: string
|
|
32
|
+
): AttachmentPreviewKind {
|
|
33
|
+
if (type?.startsWith('image/')) return 'image'
|
|
34
|
+
if (type?.startsWith('video/')) return 'video'
|
|
35
|
+
const ext = name?.split('.').pop()?.toLowerCase()
|
|
36
|
+
if (ext && IMAGE_EXTENSIONS.has(ext)) return 'image'
|
|
37
|
+
if (ext && VIDEO_EXTENSIONS.has(ext)) return 'video'
|
|
38
|
+
return 'file'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getAttachmentIconName(type: string | undefined): IconString {
|
|
42
|
+
if (type?.startsWith('image/')) return 'general.outlinedImageFile'
|
|
43
|
+
if (type?.startsWith('video/')) return 'general.outlinedVideoFile'
|
|
44
|
+
if (type?.startsWith('audio/')) return 'general.outlinedMusicFile'
|
|
45
|
+
if (type === 'application/pdf') return 'general.outlinedPdfFile'
|
|
46
|
+
return 'general.outlinedGenericFile'
|
|
47
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -17,12 +17,19 @@ type DocumentPickerCanceledResult = {
|
|
|
17
17
|
|
|
18
18
|
export type DocumentPickerResult = DocumentPickerSuccessResult | DocumentPickerCanceledResult
|
|
19
19
|
|
|
20
|
+
export interface DocumentPickerOpenOptions {
|
|
21
|
+
// MIME types the picker should restrict to. Hosts forward to the
|
|
22
|
+
// underlying picker's `type` argument. Omitted/empty means no
|
|
23
|
+
// restriction (the picker behaves as before).
|
|
24
|
+
mimeTypes?: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
interface DocumentPicker {
|
|
21
|
-
openAsync: () => Promise<DocumentPickerResult>
|
|
28
|
+
openAsync: (options?: DocumentPickerOpenOptions) => Promise<DocumentPickerResult>
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export class DocumentPickerAdapter {
|
|
25
|
-
openAsync: () => Promise<DocumentPickerResult>
|
|
32
|
+
openAsync: (options?: DocumentPickerOpenOptions) => Promise<DocumentPickerResult>
|
|
26
33
|
configured: boolean
|
|
27
34
|
|
|
28
35
|
constructor(methods?: DocumentPicker) {
|