@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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA"}
@@ -10,5 +10,6 @@ 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';
14
15
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA","sourcesContent":["export * from './session'\nexport * from './theme'\nexport * from './styles'\nexport * from './client'\nexport * from './uri'\nexport * from './cache'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './reaction_constants'\nexport * from './destructure_chat_group_graph_id'\nexport * from './convert_attachments_for_create'\nexport * from './assert_keys_are_numbers'\nexport * from './system_messages'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,sBAAsB,CAAA;AACpC,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA","sourcesContent":["export * from './session'\nexport * from './theme'\nexport * from './styles'\nexport * from './client'\nexport * from './uri'\nexport * from './cache'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './reaction_constants'\nexport * from './destructure_chat_group_graph_id'\nexport * from './convert_attachments_for_create'\nexport * from './assert_keys_are_numbers'\nexport * from './attachment_kind'\nexport * from './system_messages'\n"]}
@@ -13,11 +13,14 @@ type DocumentPickerCanceledResult = {
13
13
  assets: null;
14
14
  };
15
15
  export type DocumentPickerResult = DocumentPickerSuccessResult | DocumentPickerCanceledResult;
16
+ export interface DocumentPickerOpenOptions {
17
+ mimeTypes?: string[];
18
+ }
16
19
  interface DocumentPicker {
17
- openAsync: () => Promise<DocumentPickerResult>;
20
+ openAsync: (options?: DocumentPickerOpenOptions) => Promise<DocumentPickerResult>;
18
21
  }
19
22
  export declare class DocumentPickerAdapter {
20
- openAsync: () => Promise<DocumentPickerResult>;
23
+ openAsync: (options?: DocumentPickerOpenOptions) => Promise<DocumentPickerResult>;
21
24
  configured: boolean;
22
25
  constructor(methods?: DocumentPicker);
23
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"document_picker.d.ts","sourceRoot":"","sources":["../../../src/utils/native_adapters/document_picker.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,KAAK,2BAA2B,GAAG;IACjC,QAAQ,EAAE,KAAK,CAAA;IACf,MAAM,EAAE,mBAAmB,EAAE,CAAA;CAC9B,CAAA;AAED,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,IAAI,CAAA;IACd,MAAM,EAAE,IAAI,CAAA;CACb,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,2BAA2B,GAAG,4BAA4B,CAAA;AAE7F,UAAU,cAAc;IACtB,SAAS,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAA;CAC/C;AAED,qBAAa,qBAAqB;IAChC,SAAS,EAAE,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAA;IAC9C,UAAU,EAAE,OAAO,CAAA;gBAEP,OAAO,CAAC,EAAE,cAAc;CAIrC"}
1
+ {"version":3,"file":"document_picker.d.ts","sourceRoot":"","sources":["../../../src/utils/native_adapters/document_picker.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,KAAK,2BAA2B,GAAG;IACjC,QAAQ,EAAE,KAAK,CAAA;IACf,MAAM,EAAE,mBAAmB,EAAE,CAAA;CAC9B,CAAA;AAED,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,IAAI,CAAA;IACd,MAAM,EAAE,IAAI,CAAA;CACb,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,2BAA2B,GAAG,4BAA4B,CAAA;AAE7F,MAAM,WAAW,yBAAyB;IAIxC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,UAAU,cAAc;IACtB,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,yBAAyB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAA;CAClF;AAED,qBAAa,qBAAqB;IAChC,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,yBAAyB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAA;IACjF,UAAU,EAAE,OAAO,CAAA;gBAEP,OAAO,CAAC,EAAE,cAAc;CAIrC"}
@@ -1 +1 @@
1
- {"version":3,"file":"document_picker.js","sourceRoot":"","sources":["../../../src/utils/native_adapters/document_picker.ts"],"names":[],"mappings":"AAuBA,MAAM,OAAO,qBAAqB;IAChC,SAAS,CAAqC;IAC9C,UAAU,CAAS;IAEnB,YAAY,OAAwB;QAClC,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QACvF,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,OAAO,CAAA;IAC7B,CAAC;CACF","sourcesContent":["export type DocumentPickerAsset = {\n uri: string\n name: string\n size?: number\n mimeType?: string\n}\n\ntype DocumentPickerSuccessResult = {\n canceled: false\n assets: DocumentPickerAsset[]\n}\n\ntype DocumentPickerCanceledResult = {\n canceled: true\n assets: null\n}\n\nexport type DocumentPickerResult = DocumentPickerSuccessResult | DocumentPickerCanceledResult\n\ninterface DocumentPicker {\n openAsync: () => Promise<DocumentPickerResult>\n}\n\nexport class DocumentPickerAdapter {\n openAsync: () => Promise<DocumentPickerResult>\n configured: boolean\n\n constructor(methods?: DocumentPicker) {\n this.openAsync = methods?.openAsync ?? (async () => ({ canceled: true, assets: null }))\n this.configured = !!methods\n }\n}\n"]}
1
+ {"version":3,"file":"document_picker.js","sourceRoot":"","sources":["../../../src/utils/native_adapters/document_picker.ts"],"names":[],"mappings":"AA8BA,MAAM,OAAO,qBAAqB;IAChC,SAAS,CAAwE;IACjF,UAAU,CAAS;IAEnB,YAAY,OAAwB;QAClC,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QACvF,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,OAAO,CAAA;IAC7B,CAAC;CACF","sourcesContent":["export type DocumentPickerAsset = {\n uri: string\n name: string\n size?: number\n mimeType?: string\n}\n\ntype DocumentPickerSuccessResult = {\n canceled: false\n assets: DocumentPickerAsset[]\n}\n\ntype DocumentPickerCanceledResult = {\n canceled: true\n assets: null\n}\n\nexport type DocumentPickerResult = DocumentPickerSuccessResult | DocumentPickerCanceledResult\n\nexport interface DocumentPickerOpenOptions {\n // MIME types the picker should restrict to. Hosts forward to the\n // underlying picker's `type` argument. Omitted/empty means no\n // restriction (the picker behaves as before).\n mimeTypes?: string[]\n}\n\ninterface DocumentPicker {\n openAsync: (options?: DocumentPickerOpenOptions) => Promise<DocumentPickerResult>\n}\n\nexport class DocumentPickerAdapter {\n openAsync: (options?: DocumentPickerOpenOptions) => Promise<DocumentPickerResult>\n configured: boolean\n\n constructor(methods?: DocumentPicker) {\n this.openAsync = methods?.openAsync ?? (async () => ({ canceled: true, assets: null }))\n this.configured = !!methods\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.33.1",
3
+ "version": "3.33.2-qa-664.1",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -27,7 +27,7 @@
27
27
  "@fortawesome/react-native-fontawesome": "^0.3.2",
28
28
  "lodash-inflection": "^1.5.0",
29
29
  "react-compiler-runtime": "^1.0.0",
30
- "rn-emoji-keyboard": "^1.7.0"
30
+ "rn-emoji-keyboard": "1.7.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@planningcenter/datetime-fmt": ">=2.0.0",
@@ -65,5 +65,5 @@
65
65
  "react-native-url-polyfill": "^2.0.0",
66
66
  "typescript": "~5.9.2"
67
67
  },
68
- "gitHead": "305574cb6776378d3c6f41d18302bf0ef1949961"
68
+ "gitHead": "35b43ec0bbb57ea9220ce95b0f4e49acf7779f87"
69
69
  }
@@ -1,7 +1,8 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage'
2
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { QueryClientProvider } from '@tanstack/react-query'
3
3
  import { renderHook, act } from '@testing-library/react-hooks'
4
4
  import React, { useContext, Suspense } from 'react'
5
+ import { buildTestQueryClient } from '../../__utils__/query_client'
5
6
  import {
6
7
  SessionContext,
7
8
  SessionContextProvider,
@@ -67,13 +68,7 @@ const waitForQuery = async () => {
67
68
  }
68
69
 
69
70
  const createWrapper = (config: SessionContextConfig) => {
70
- const queryClient = new QueryClient({
71
- defaultOptions: {
72
- queries: {
73
- retry: false,
74
- },
75
- },
76
- })
71
+ const queryClient = buildTestQueryClient()
77
72
 
78
73
  return ({ children }: { children: React.ReactNode }) => (
79
74
  <QueryClientProvider client={queryClient}>
@@ -0,0 +1,15 @@
1
+ import { getNextPageParamFromMeta } from '../../hooks/paginator_meta'
2
+
3
+ describe('getNextPageParamFromMeta', () => {
4
+ it.each([
5
+ [undefined, undefined],
6
+ [{}, undefined],
7
+ [{ idLt: '01ABC' }, { where: { id_lt: '01ABC' } }],
8
+ [{ idLte: '01ABC' }, { where: { id_lte: '01ABC' } }],
9
+ [{ idGt: '01ABC' }, { where: { id_gt: '01ABC' } }],
10
+ [{ idGte: '01ABC' }, { where: { id_gte: '01ABC' } }],
11
+ [{ offset: '40' }, { offset: 40 }],
12
+ ])('maps %p to %p', (input, expected) => {
13
+ expect(getNextPageParamFromMeta(input)).toEqual(expected)
14
+ })
15
+ })
@@ -1,6 +1,7 @@
1
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
2
  import { renderHook } from '@testing-library/react-native'
3
3
  import React from 'react'
4
+ import { buildTestQueryClient } from '../../__utils__/query_client'
4
5
  import { ChatProvider, CreateChatThemeProps } from '../../contexts/chat_context'
5
6
  import { useTheme } from '../../hooks'
6
7
 
@@ -14,11 +15,10 @@ let themeProps: CreateChatThemeProps = {
14
15
  let queryClient: QueryClient
15
16
 
16
17
  beforeEach(() => {
17
- queryClient = new QueryClient({
18
+ queryClient = buildTestQueryClient({
18
19
  defaultOptions: {
19
20
  queries: {
20
- retry: false,
21
- refetchInterval: false, // Disable refetch intervals in tests
21
+ refetchInterval: false,
22
22
  refetchOnWindowFocus: false,
23
23
  refetchOnReconnect: false,
24
24
  },
@@ -1,7 +1,8 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage'
2
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { QueryClientProvider } from '@tanstack/react-query'
3
3
  import { renderHook, act } from '@testing-library/react-hooks'
4
4
  import React, { Suspense } from 'react'
5
+ import { buildTestQueryClient } from '../../__utils__/query_client'
5
6
  import { useAsyncStorage } from '../../hooks/use_async_storage'
6
7
 
7
8
  jest.mock('@react-native-async-storage/async-storage')
@@ -14,13 +15,7 @@ afterAll(() => {
14
15
  const mockAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage>
15
16
 
16
17
  const createWrapper = () => {
17
- const queryClient = new QueryClient({
18
- defaultOptions: {
19
- queries: {
20
- retry: false,
21
- },
22
- },
23
- })
18
+ const queryClient = buildTestQueryClient()
24
19
 
25
20
  return ({ children }: { children: React.ReactNode }) => (
26
21
  <QueryClientProvider client={queryClient}>
@@ -1,6 +1,7 @@
1
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
1
+ import { QueryClientProvider } from '@tanstack/react-query'
2
2
  import { renderHook, act } from '@testing-library/react-hooks'
3
3
  import React from 'react'
4
+ import { buildTestQueryClient } from '../../__utils__/query_client'
4
5
  import { useApiClient } from '../../hooks/use_api_client'
5
6
  import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
6
7
  import { useChatConfiguration } from '../../hooks/use_chat_configuration'
@@ -18,7 +19,7 @@ const mockedUseChatConfiguration = useChatConfiguration as jest.MockedFunction<
18
19
  >
19
20
 
20
21
  const createWrapper = () => {
21
- const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
22
+ const queryClient = buildTestQueryClient()
22
23
  return ({ children }: { children: React.ReactNode }) => (
23
24
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
24
25
  )
@@ -1,8 +1,10 @@
1
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
1
+ import { QueryClientProvider } from '@tanstack/react-query'
2
2
  import { renderHook, act } from '@testing-library/react-hooks'
3
3
  import React, { Suspense } from 'react'
4
+ import { buildTestQueryClient } from '../../__utils__/query_client'
4
5
  import {
5
6
  FALLBACK_ALLOWED_FILE_EXTENSIONS,
7
+ FALLBACK_ALLOWED_MIME_TYPES,
6
8
  FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
7
9
  FALLBACK_MAX_FILE_SIZE_IN_BYTES,
8
10
  } from '../../hooks/attachments/fallback_chat_configuration'
@@ -10,9 +12,7 @@ import * as useApiClientModule from '../../hooks/use_api_client'
10
12
  import { useChatConfiguration } from '../../hooks/use_chat_configuration'
11
13
 
12
14
  const createWrapper = () => {
13
- const queryClient = new QueryClient({
14
- defaultOptions: { queries: { retry: false } },
15
- })
15
+ const queryClient = buildTestQueryClient()
16
16
 
17
17
  return ({ children }: { children: React.ReactNode }) => (
18
18
  <QueryClientProvider client={queryClient}>
@@ -47,6 +47,7 @@ describe('useChatConfiguration', () => {
47
47
  type: 'ChatConfiguration',
48
48
  id: 'current',
49
49
  allowedFileExtensions: ['.pdf', '.jpg'],
50
+ allowedMimeTypes: ['application/pdf', 'image/jpeg'],
50
51
  maxFileSizeInBytes: 1000,
51
52
  maxAttachmentsPerMessage: 3,
52
53
  },
@@ -60,11 +61,34 @@ describe('useChatConfiguration', () => {
60
61
 
61
62
  expect(result.current).toEqual({
62
63
  allowedFileExtensions: ['.pdf', '.jpg'],
64
+ allowedMimeTypes: ['application/pdf', 'image/jpeg'],
63
65
  maxFileSizeInBytes: 1000,
64
66
  maxAttachmentsPerMessage: 3,
65
67
  })
66
68
  })
67
69
 
70
+ it('falls back to FALLBACK_ALLOWED_MIME_TYPES when the server omits the field', async () => {
71
+ // During server-side rollout the field may be absent from the response.
72
+ mockApiClient(() =>
73
+ Promise.resolve({
74
+ data: {
75
+ type: 'ChatConfiguration',
76
+ id: 'current',
77
+ allowedFileExtensions: ['.pdf'],
78
+ maxFileSizeInBytes: 1000,
79
+ maxAttachmentsPerMessage: 3,
80
+ },
81
+ links: {},
82
+ meta: {},
83
+ })
84
+ )
85
+
86
+ const { result } = renderHook(() => useChatConfiguration(), { wrapper: createWrapper() })
87
+ await waitForQuery()
88
+
89
+ expect(result.current.allowedMimeTypes).toEqual(FALLBACK_ALLOWED_MIME_TYPES)
90
+ })
91
+
68
92
  it('returns fallback values when the API rejects', async () => {
69
93
  mockApiClient(() => Promise.reject(new Error('boom')))
70
94
 
@@ -73,6 +97,7 @@ describe('useChatConfiguration', () => {
73
97
 
74
98
  expect(result.current).toEqual({
75
99
  allowedFileExtensions: FALLBACK_ALLOWED_FILE_EXTENSIONS,
100
+ allowedMimeTypes: FALLBACK_ALLOWED_MIME_TYPES,
76
101
  maxFileSizeInBytes: FALLBACK_MAX_FILE_SIZE_IN_BYTES,
77
102
  maxAttachmentsPerMessage: FALLBACK_MAX_ATTACHMENTS_PER_MESSAGE,
78
103
  })
@@ -0,0 +1,14 @@
1
+ import { QueryClient, QueryClientConfig } from '@tanstack/react-query'
2
+ import { merge } from 'lodash'
3
+
4
+ const baseConfig: QueryClientConfig = {
5
+ defaultOptions: {
6
+ queries: {
7
+ retry: false,
8
+ gcTime: 0,
9
+ },
10
+ },
11
+ }
12
+
13
+ export const buildTestQueryClient = (overrides: QueryClientConfig = {}) =>
14
+ new QueryClient(merge({}, baseConfig, overrides))
@@ -3,7 +3,7 @@ import React from 'react'
3
3
  import { StyleSheet, View } from 'react-native'
4
4
  import { useTheme } from '../../../hooks'
5
5
  import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
6
- import { Linking } from '../../../utils'
6
+ import { Linking, getAttachmentIconName } from '../../../utils'
7
7
  import { tokens } from '../../../vendor/tapestry/tokens'
8
8
  import { Icon } from '../../display'
9
9
  import { AttachmentCard, AttachmentCardTitle } from './attachment_card'
@@ -75,16 +75,3 @@ const useStyles = () => {
75
75
  },
76
76
  })
77
77
  }
78
-
79
- export function getAttachmentIconName(type: string) {
80
- const isImage = type.startsWith('image/')
81
- const isVideo = type.startsWith('video/')
82
- const isAudio = type.startsWith('audio/')
83
- const isPdf = type === 'application/pdf'
84
-
85
- if (isImage) return 'general.outlinedImageFile'
86
- if (isVideo) return 'general.outlinedVideoFile'
87
- if (isAudio) return 'general.outlinedMusicFile'
88
- if (isPdf) return 'general.outlinedPdfFile'
89
- return 'general.outlinedGenericFile'
90
- }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+ import { FileAttachment } from '../../../types/resources/denormalized_attachment_resource_for_create'
3
+ import { FileAttachmentPreview } from '../../display'
4
+
5
+ interface Props {
6
+ status: FileAttachment['status']
7
+ name: string
8
+ contentType?: string
9
+ removeAttachment: () => void
10
+ }
11
+
12
+ export function MessageFormAttachmentFile({ name, contentType, status, removeAttachment }: Props) {
13
+ const loading = status === 'uploading'
14
+ const error = status === 'error'
15
+
16
+ return (
17
+ <FileAttachmentPreview
18
+ name={name}
19
+ contentType={contentType}
20
+ onRemovePress={removeAttachment}
21
+ loading={loading}
22
+ error={error}
23
+ hideRemoveButton={loading}
24
+ />
25
+ )
26
+ }
@@ -11,7 +11,6 @@ interface Props {
11
11
  export function MessageFormAttachmentVideo({ name, status, removeAttachment }: Props) {
12
12
  const loading = status === 'uploading'
13
13
  const error = status === 'error'
14
- const ready = status === 'success'
15
14
 
16
15
  return (
17
16
  <VideoAttachmentPreview
@@ -19,7 +18,7 @@ export function MessageFormAttachmentVideo({ name, status, removeAttachment }: P
19
18
  onRemovePress={removeAttachment}
20
19
  loading={loading}
21
20
  error={error}
22
- hideRemoveButton={!ready}
21
+ hideRemoveButton={loading}
23
22
  />
24
23
  )
25
24
  }
@@ -23,6 +23,7 @@ import {
23
23
  } from '../../hooks'
24
24
  import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
25
25
  import { useBroadcastTypingStatus } from '../../hooks/use_broadcast_typing_status'
26
+ import { useChatConfiguration } from '../../hooks/use_chat_configuration'
26
27
  import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
27
28
  import { useMessageDraft } from '../../hooks/use_message_draft'
28
29
  import { ConversationScreenProps } from '../../screens/conversation_screen'
@@ -36,6 +37,7 @@ import {
36
37
  platformFontWeightMedium,
37
38
  platformPressedOpacityStyle,
38
39
  } from '../../utils'
40
+ import { pickAttachmentPreviewKind } from '../../utils/attachment_kind'
39
41
  import {
40
42
  DocumentPicker,
41
43
  DocumentPickerResult,
@@ -46,6 +48,7 @@ import {
46
48
  import { tokens } from '../../vendor/tapestry/tokens'
47
49
  import { Button } from '../display/button'
48
50
  import BannerPrimitive from '../primitive/banner_primitive'
51
+ import { MessageFormAttachmentFile } from './message_form/message_form_attachment_file'
49
52
  import { MessageFormAttachmentImage } from './message_form/message_form_attachment_image'
50
53
  import { MessageFormAttachmentVideo } from './message_form/message_form_attachment_video'
51
54
 
@@ -286,15 +289,28 @@ function MessageFormAttachments() {
286
289
  contentContainerStyle={styles.messageFormAttachments}
287
290
  >
288
291
  {attachments.map(attachment => {
289
- if (attachment.file.type.startsWith('video/')) {
292
+ const kind = pickAttachmentPreviewKind(attachment.file.type, attachment.file.name)
293
+ const remove = () => attachmentUploader?.removeAttachment(attachment)
294
+
295
+ if (kind === 'video') {
290
296
  return (
291
297
  <MessageFormAttachmentVideo
292
298
  key={attachment.file.uri}
293
299
  name={attachment.file.name}
294
300
  status={attachment.status}
295
- removeAttachment={() => {
296
- attachmentUploader?.removeAttachment(attachment)
297
- }}
301
+ removeAttachment={remove}
302
+ />
303
+ )
304
+ }
305
+
306
+ if (kind === 'file') {
307
+ return (
308
+ <MessageFormAttachmentFile
309
+ key={attachment.file.uri}
310
+ name={attachment.file.name}
311
+ contentType={attachment.file.type}
312
+ status={attachment.status}
313
+ removeAttachment={remove}
298
314
  />
299
315
  )
300
316
  }
@@ -307,9 +323,7 @@ function MessageFormAttachments() {
307
323
  status={attachment.status}
308
324
  width={attachment.file.width}
309
325
  height={attachment.file.height}
310
- removeAttachment={() => {
311
- attachmentUploader?.removeAttachment(attachment)
312
- }}
326
+ removeAttachment={remove}
313
327
  />
314
328
  )
315
329
  })}
@@ -503,6 +517,7 @@ function MessageFormAttachmentPicker() {
503
517
  const styles = useMessageFormStyles()
504
518
  const { usingGiphy, attachmentUploader, currentlyEditingMessage } =
505
519
  React.useContext(MessageFormContext)
520
+ const { allowedMimeTypes } = useChatConfiguration()
506
521
  const [isOpen, setIsOpen] = useState(false)
507
522
 
508
523
  function uploadImagePickerResult(result: ImagePickerResult) {
@@ -561,7 +576,7 @@ function MessageFormAttachmentPicker() {
561
576
 
562
577
  const pickFile = async () => {
563
578
  setIsOpen(false)
564
- let result = await DocumentPicker.openAsync()
579
+ let result = await DocumentPicker.openAsync({ mimeTypes: allowedMimeTypes })
565
580
  if (!result.canceled) {
566
581
  uploadDocumentPickerResult(result)
567
582
  }
@@ -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
@@ -0,0 +1,13 @@
1
+ import { NextPageCursor } from '../types/api_primitives'
2
+ import { RequestData } from '../utils/client'
3
+
4
+ export const getNextPageParamFromMeta = (
5
+ next: NextPageCursor = {}
6
+ ): Partial<RequestData> | undefined => {
7
+ if (next.idLt) return { where: { id_lt: next.idLt } }
8
+ if (next.idGt) return { where: { id_gt: next.idGt } }
9
+ if (next.idLte) return { where: { id_lte: next.idLte } }
10
+ if (next.idGte) return { where: { id_gte: next.idGte } }
11
+ if (next.offset) return { offset: Number(next.offset) }
12
+ return undefined
13
+ }