@planningcenter/chat-react-native 3.33.1 → 3.33.2-qa-664.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/build/components/conversation/attachments/generic_file_attachment.d.ts +0 -1
  2. package/build/components/conversation/attachments/generic_file_attachment.d.ts.map +1 -1
  3. package/build/components/conversation/attachments/generic_file_attachment.js +1 -16
  4. package/build/components/conversation/attachments/generic_file_attachment.js.map +1 -1
  5. package/build/components/conversation/message_form/message_form_attachment_file.d.ts +11 -0
  6. package/build/components/conversation/message_form/message_form_attachment_file.d.ts.map +1 -0
  7. package/build/components/conversation/message_form/message_form_attachment_file.js +8 -0
  8. package/build/components/conversation/message_form/message_form_attachment_file.js.map +1 -0
  9. package/build/components/conversation/message_form/message_form_attachment_video.d.ts.map +1 -1
  10. package/build/components/conversation/message_form/message_form_attachment_video.js +1 -2
  11. package/build/components/conversation/message_form/message_form_attachment_video.js.map +1 -1
  12. package/build/components/conversation/message_form.d.ts.map +1 -1
  13. package/build/components/conversation/message_form.js +13 -8
  14. package/build/components/conversation/message_form.js.map +1 -1
  15. package/build/components/display/file_attachment_preview.d.ts +11 -0
  16. package/build/components/display/file_attachment_preview.d.ts.map +1 -0
  17. package/build/components/display/file_attachment_preview.js +95 -0
  18. package/build/components/display/file_attachment_preview.js.map +1 -0
  19. package/build/components/display/image.d.ts.map +1 -1
  20. package/build/components/display/image.js +4 -1
  21. package/build/components/display/image.js.map +1 -1
  22. package/build/components/display/index.d.ts +1 -0
  23. package/build/components/display/index.d.ts.map +1 -1
  24. package/build/components/display/index.js +1 -0
  25. package/build/components/display/index.js.map +1 -1
  26. package/build/hooks/attachments/fallback_chat_configuration.d.ts +1 -0
  27. package/build/hooks/attachments/fallback_chat_configuration.d.ts.map +1 -1
  28. package/build/hooks/attachments/fallback_chat_configuration.js +23 -0
  29. package/build/hooks/attachments/fallback_chat_configuration.js.map +1 -1
  30. package/build/hooks/paginator_meta.d.ts +4 -0
  31. package/build/hooks/paginator_meta.d.ts.map +1 -0
  32. package/build/hooks/paginator_meta.js +14 -0
  33. package/build/hooks/paginator_meta.js.map +1 -0
  34. package/build/hooks/use_api.d.ts.map +1 -1
  35. package/build/hooks/use_api.js +2 -9
  36. package/build/hooks/use_api.js.map +1 -1
  37. package/build/hooks/use_chat_configuration.d.ts +1 -0
  38. package/build/hooks/use_chat_configuration.d.ts.map +1 -1
  39. package/build/hooks/use_chat_configuration.js +9 -1
  40. package/build/hooks/use_chat_configuration.js.map +1 -1
  41. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  42. package/build/hooks/use_suspense_api.js +2 -9
  43. package/build/hooks/use_suspense_api.js.map +1 -1
  44. package/build/index.d.ts +1 -0
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +1 -0
  47. package/build/index.js.map +1 -1
  48. package/build/screens/avatar_picker/emoji_tab.d.ts.map +1 -1
  49. package/build/screens/avatar_picker/emoji_tab.js +10 -1
  50. package/build/screens/avatar_picker/emoji_tab.js.map +1 -1
  51. package/build/types/api_primitives.d.ts +8 -1
  52. package/build/types/api_primitives.d.ts.map +1 -1
  53. package/build/types/api_primitives.js.map +1 -1
  54. package/build/types/resources/chat_configuration_resource.d.ts +1 -0
  55. package/build/types/resources/chat_configuration_resource.d.ts.map +1 -1
  56. package/build/types/resources/chat_configuration_resource.js.map +1 -1
  57. package/build/utils/attachment_kind.d.ts +5 -0
  58. package/build/utils/attachment_kind.d.ts.map +1 -0
  59. package/build/utils/attachment_kind.js +46 -0
  60. package/build/utils/attachment_kind.js.map +1 -0
  61. package/build/utils/index.d.ts +1 -0
  62. package/build/utils/index.d.ts.map +1 -1
  63. package/build/utils/index.js +1 -0
  64. package/build/utils/index.js.map +1 -1
  65. package/build/utils/native_adapters/document_picker.d.ts +5 -2
  66. package/build/utils/native_adapters/document_picker.d.ts.map +1 -1
  67. package/build/utils/native_adapters/document_picker.js.map +1 -1
  68. package/package.json +3 -3
  69. package/src/__tests__/contexts/session_context.tsx +3 -8
  70. package/src/__tests__/hooks/paginator_meta.test.ts +15 -0
  71. package/src/__tests__/hooks/useTheme.tsx +3 -3
  72. package/src/__tests__/hooks/use_async_storage.test.tsx +3 -8
  73. package/src/__tests__/hooks/use_attachment_uploader.test.tsx +3 -2
  74. package/src/__tests__/hooks/use_chat_configuration.test.tsx +29 -4
  75. package/src/__utils__/query_client.ts +14 -0
  76. package/src/components/conversation/attachments/generic_file_attachment.tsx +1 -14
  77. package/src/components/conversation/message_form/message_form_attachment_file.tsx +26 -0
  78. package/src/components/conversation/message_form/message_form_attachment_video.tsx +1 -2
  79. package/src/components/conversation/message_form.tsx +23 -8
  80. package/src/components/display/file_attachment_preview.tsx +135 -0
  81. package/src/components/display/image.tsx +5 -0
  82. package/src/components/display/index.ts +1 -0
  83. package/src/hooks/attachments/fallback_chat_configuration.ts +24 -0
  84. package/src/hooks/paginator_meta.ts +13 -0
  85. package/src/hooks/use_api.ts +2 -14
  86. package/src/hooks/use_chat_configuration.ts +9 -0
  87. package/src/hooks/use_suspense_api.ts +2 -14
  88. package/src/index.tsx +1 -0
  89. package/src/screens/avatar_picker/emoji_tab.tsx +13 -1
  90. package/src/types/api_primitives.ts +9 -1
  91. package/src/types/resources/chat_configuration_resource.ts +4 -0
  92. package/src/utils/__tests__/attachment_kind.test.ts +37 -0
  93. package/src/utils/attachment_kind.ts +47 -0
  94. package/src/utils/index.ts +1 -0
  95. package/src/utils/native_adapters/document_picker.ts +9 -2
@@ -6,6 +6,7 @@ import {
6
6
  } from '@tanstack/react-query'
7
7
  import { ApiCollection, ApiResource, FailedResponse, ResourceObject } from '../types'
8
8
  import { GetRequest, RequestData } from '../utils/client'
9
+ import { getNextPageParamFromMeta } from './paginator_meta'
9
10
  import { App, useApiClient } from './use_api_client'
10
11
  import { getRequestQueryKey, RequestQueryKey } from './use_suspense_api'
11
12
 
@@ -31,11 +32,6 @@ export const useApiGet = <T extends ResourceObject | ResourceObject[]>(args: Api
31
32
  return { ...data, ...query }
32
33
  }
33
34
 
34
- type NextMeta = Partial<{
35
- offset: string
36
- idLt: string
37
- }>
38
-
39
35
  export type PaginatorOptions = Omit<
40
36
  AnyUseSuspenseInfiniteQueryOptions,
41
37
  'getNextPageParam' | 'initialPageParam' | 'queryFn' | 'queryKey'
@@ -68,15 +64,7 @@ export const useApiPaginator = <T extends ResourceObject>(
68
64
  })
69
65
  },
70
66
  initialPageParam: {} as Partial<RequestData>,
71
- getNextPageParam: lastPage => {
72
- const next: NextMeta = lastPage.meta?.next || {}
73
- const { offset, idLt } = next
74
-
75
- if (idLt) return { where: { id_lt: idLt } }
76
- if (offset) return { offset: Number(offset) }
77
-
78
- return undefined
79
- },
67
+ getNextPageParam: lastPage => getNextPageParamFromMeta(lastPage.meta?.next),
80
68
  enabled: args.enabled,
81
69
  ...(opts || {}),
82
70
  })
@@ -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
  },
@@ -10,6 +10,7 @@ import { FailedResponse } from '../types/api_primitives'
10
10
  import { Log } from '../utils'
11
11
  import { GetRequest, RequestData } from '../utils/client'
12
12
  import { ResponseError } from '../utils/response_error'
13
+ import { getNextPageParamFromMeta } from './paginator_meta'
13
14
  import { App, useApiClient } from './use_api_client'
14
15
 
15
16
  interface SuspenseGetOptions extends GetRequest {
@@ -43,11 +44,6 @@ export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(
43
44
  return { ...data, ...query }
44
45
  }
45
46
 
46
- type NextMeta = Partial<{
47
- offset: string
48
- idLt: string
49
- }>
50
-
51
47
  export type SuspensePaginatorOptions = Omit<
52
48
  AnyUseSuspenseInfiniteQueryOptions,
53
49
  'getNextPageParam' | 'initialPageParam' | 'queryFn' | 'queryKey'
@@ -84,15 +80,7 @@ export const useSuspensePaginator = <T extends ResourceObject>(
84
80
  .catch(throwResponseError)
85
81
  },
86
82
  initialPageParam: {} as Partial<RequestData>,
87
- getNextPageParam: lastPage => {
88
- const next: NextMeta = lastPage.meta?.next || {}
89
- const { offset, idLt } = next
90
-
91
- if (idLt) return { where: { id_lt: idLt } }
92
- if (offset) return { offset: Number(offset) }
93
-
94
- return undefined
95
- },
83
+ getNextPageParam: lastPage => getNextPageParamFromMeta(lastPage.meta?.next),
96
84
  ...(opts || {}),
97
85
  })
98
86
 
package/src/index.tsx CHANGED
@@ -12,5 +12,6 @@ export * from './types'
12
12
  export { platformFontWeightBold, Session, TemporaryDefaultColorsType, Uri } from './utils'
13
13
  export * from './utils/client'
14
14
  export * from './utils/native_adapters'
15
+ export { ResponseError } from './utils/response_error'
15
16
  export { default as Event } from './polyfills/events/Event'
16
17
  export { CustomEvent } from './polyfills/events/CustomEvent'
@@ -1,8 +1,19 @@
1
1
  import React, { useCallback } from 'react'
2
2
  import { StyleSheet, View } from 'react-native'
3
- import { EmojiKeyboard, type EmojiType } from 'rn-emoji-keyboard'
3
+ import { EmojiKeyboard, type EmojiType, type EmojisByCategory } from 'rn-emoji-keyboard'
4
+ // rn-emoji-keyboard exposes no public exclusion API, so we reach into its
5
+ // internal src/ tree for the emoji JSON. Version is pinned in package.json
6
+ // — verify this path still resolves before bumping rn-emoji-keyboard.
7
+ import emojiData from 'rn-emoji-keyboard/src/assets/emojis.json'
4
8
  import { useTheme } from '../../hooks'
5
9
 
10
+ const BLOCKED_EMOJIS = new Set(['🖕'])
11
+
12
+ const filteredEmojis = (emojiData as EmojisByCategory[]).map(category => ({
13
+ ...category,
14
+ data: category.data.filter(e => !BLOCKED_EMOJIS.has(e.emoji)),
15
+ }))
16
+
6
17
  interface EmojiTabProps {
7
18
  onEmojiSelect: (emoji: string) => void
8
19
  }
@@ -44,6 +55,7 @@ export function EmojiTab({ onEmojiSelect }: EmojiTabProps) {
44
55
  <View style={styles.container}>
45
56
  <EmojiKeyboard
46
57
  categoryPosition="top"
58
+ emojisByCategory={filteredEmojis}
47
59
  onEmojiSelected={handleEmojiSelected}
48
60
  enableSearchBar
49
61
  enableRecentlyUsed
@@ -27,10 +27,18 @@ export interface FailedResponse extends Response {
27
27
  errors: ErrorObject[]
28
28
  }
29
29
 
30
+ export interface NextPageCursor {
31
+ offset?: string
32
+ idLt?: string
33
+ idLte?: string
34
+ idGt?: string
35
+ idGte?: string
36
+ }
37
+
30
38
  export interface CollectionMeta {
31
39
  count: number
32
40
  totalCount: number
33
- next?: Record<string, unknown>
41
+ next?: NextPageCursor
34
42
  parent?: ResourceObject
35
43
  [attributeName: string]: unknown
36
44
  }
@@ -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
+ }
@@ -10,4 +10,5 @@ export * from './reaction_constants'
10
10
  export * from './destructure_chat_group_graph_id'
11
11
  export * from './convert_attachments_for_create'
12
12
  export * from './assert_keys_are_numbers'
13
+ export * from './attachment_kind'
13
14
  export * from './system_messages'
@@ -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) {