@parlr/react-native 0.1.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.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +918 -0
  3. package/lib/commonjs/components/AttachmentPicker.js +292 -0
  4. package/lib/commonjs/components/AttachmentPicker.js.map +1 -0
  5. package/lib/commonjs/components/AttachmentPreview.js +200 -0
  6. package/lib/commonjs/components/AttachmentPreview.js.map +1 -0
  7. package/lib/commonjs/components/ChatBubble.js +391 -0
  8. package/lib/commonjs/components/ChatBubble.js.map +1 -0
  9. package/lib/commonjs/components/EmptyState.js +115 -0
  10. package/lib/commonjs/components/EmptyState.js.map +1 -0
  11. package/lib/commonjs/components/ParlrChat.js +745 -0
  12. package/lib/commonjs/components/ParlrChat.js.map +1 -0
  13. package/lib/commonjs/components/ParlrConversationList.js +509 -0
  14. package/lib/commonjs/components/ParlrConversationList.js.map +1 -0
  15. package/lib/commonjs/components/PreChatForm.js +263 -0
  16. package/lib/commonjs/components/PreChatForm.js.map +1 -0
  17. package/lib/commonjs/components/RichMessage.js +284 -0
  18. package/lib/commonjs/components/RichMessage.js.map +1 -0
  19. package/lib/commonjs/components/SatisfactionSurvey.js +292 -0
  20. package/lib/commonjs/components/SatisfactionSurvey.js.map +1 -0
  21. package/lib/commonjs/components/TypingIndicator.js +86 -0
  22. package/lib/commonjs/components/TypingIndicator.js.map +1 -0
  23. package/lib/commonjs/core/api.js +310 -0
  24. package/lib/commonjs/core/api.js.map +1 -0
  25. package/lib/commonjs/core/config.js +40 -0
  26. package/lib/commonjs/core/config.js.map +1 -0
  27. package/lib/commonjs/core/errors.js +73 -0
  28. package/lib/commonjs/core/errors.js.map +1 -0
  29. package/lib/commonjs/core/offlineQueue.js +89 -0
  30. package/lib/commonjs/core/offlineQueue.js.map +1 -0
  31. package/lib/commonjs/core/pushNotifications.js +21 -0
  32. package/lib/commonjs/core/pushNotifications.js.map +1 -0
  33. package/lib/commonjs/core/session.js +130 -0
  34. package/lib/commonjs/core/session.js.map +1 -0
  35. package/lib/commonjs/core/theme.js +110 -0
  36. package/lib/commonjs/core/theme.js.map +1 -0
  37. package/lib/commonjs/core/types.js +6 -0
  38. package/lib/commonjs/core/types.js.map +1 -0
  39. package/lib/commonjs/core/websocket.js +245 -0
  40. package/lib/commonjs/core/websocket.js.map +1 -0
  41. package/lib/commonjs/hooks/useChat.js +462 -0
  42. package/lib/commonjs/hooks/useChat.js.map +1 -0
  43. package/lib/commonjs/hooks/useParlr.js +44 -0
  44. package/lib/commonjs/hooks/useParlr.js.map +1 -0
  45. package/lib/commonjs/index.js +185 -0
  46. package/lib/commonjs/index.js.map +1 -0
  47. package/lib/commonjs/package.json +1 -0
  48. package/lib/commonjs/provider/ParlrContext.js +38 -0
  49. package/lib/commonjs/provider/ParlrContext.js.map +1 -0
  50. package/lib/commonjs/provider/ParlrProvider.js +256 -0
  51. package/lib/commonjs/provider/ParlrProvider.js.map +1 -0
  52. package/lib/module/components/AttachmentPicker.js +287 -0
  53. package/lib/module/components/AttachmentPicker.js.map +1 -0
  54. package/lib/module/components/AttachmentPreview.js +195 -0
  55. package/lib/module/components/AttachmentPreview.js.map +1 -0
  56. package/lib/module/components/ChatBubble.js +386 -0
  57. package/lib/module/components/ChatBubble.js.map +1 -0
  58. package/lib/module/components/EmptyState.js +110 -0
  59. package/lib/module/components/EmptyState.js.map +1 -0
  60. package/lib/module/components/ParlrChat.js +740 -0
  61. package/lib/module/components/ParlrChat.js.map +1 -0
  62. package/lib/module/components/ParlrConversationList.js +504 -0
  63. package/lib/module/components/ParlrConversationList.js.map +1 -0
  64. package/lib/module/components/PreChatForm.js +258 -0
  65. package/lib/module/components/PreChatForm.js.map +1 -0
  66. package/lib/module/components/RichMessage.js +280 -0
  67. package/lib/module/components/RichMessage.js.map +1 -0
  68. package/lib/module/components/SatisfactionSurvey.js +287 -0
  69. package/lib/module/components/SatisfactionSurvey.js.map +1 -0
  70. package/lib/module/components/TypingIndicator.js +81 -0
  71. package/lib/module/components/TypingIndicator.js.map +1 -0
  72. package/lib/module/core/api.js +305 -0
  73. package/lib/module/core/api.js.map +1 -0
  74. package/lib/module/core/config.js +36 -0
  75. package/lib/module/core/config.js.map +1 -0
  76. package/lib/module/core/errors.js +64 -0
  77. package/lib/module/core/errors.js.map +1 -0
  78. package/lib/module/core/offlineQueue.js +82 -0
  79. package/lib/module/core/offlineQueue.js.map +1 -0
  80. package/lib/module/core/pushNotifications.js +16 -0
  81. package/lib/module/core/pushNotifications.js.map +1 -0
  82. package/lib/module/core/session.js +122 -0
  83. package/lib/module/core/session.js.map +1 -0
  84. package/lib/module/core/theme.js +105 -0
  85. package/lib/module/core/theme.js.map +1 -0
  86. package/lib/module/core/types.js +4 -0
  87. package/lib/module/core/types.js.map +1 -0
  88. package/lib/module/core/websocket.js +241 -0
  89. package/lib/module/core/websocket.js.map +1 -0
  90. package/lib/module/hooks/useChat.js +458 -0
  91. package/lib/module/hooks/useChat.js.map +1 -0
  92. package/lib/module/hooks/useParlr.js +40 -0
  93. package/lib/module/hooks/useParlr.js.map +1 -0
  94. package/lib/module/index.js +58 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/package.json +1 -0
  97. package/lib/module/provider/ParlrContext.js +35 -0
  98. package/lib/module/provider/ParlrContext.js.map +1 -0
  99. package/lib/module/provider/ParlrProvider.js +251 -0
  100. package/lib/module/provider/ParlrProvider.js.map +1 -0
  101. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts +23 -0
  102. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts +16 -0
  104. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/components/ChatBubble.d.ts +14 -0
  106. package/lib/typescript/commonjs/components/ChatBubble.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/components/EmptyState.d.ts +10 -0
  108. package/lib/typescript/commonjs/components/EmptyState.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/components/ParlrChat.d.ts +34 -0
  110. package/lib/typescript/commonjs/components/ParlrChat.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts +17 -0
  112. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/components/PreChatForm.d.ts +20 -0
  114. package/lib/typescript/commonjs/components/PreChatForm.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/components/RichMessage.d.ts +41 -0
  116. package/lib/typescript/commonjs/components/RichMessage.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts +17 -0
  118. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/components/TypingIndicator.d.ts +7 -0
  120. package/lib/typescript/commonjs/components/TypingIndicator.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/core/api.d.ts +37 -0
  122. package/lib/typescript/commonjs/core/api.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/core/config.d.ts +9 -0
  124. package/lib/typescript/commonjs/core/config.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/core/errors.d.ts +35 -0
  126. package/lib/typescript/commonjs/core/errors.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/core/offlineQueue.d.ts +16 -0
  128. package/lib/typescript/commonjs/core/offlineQueue.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/core/pushNotifications.d.ts +6 -0
  130. package/lib/typescript/commonjs/core/pushNotifications.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/core/session.d.ts +15 -0
  132. package/lib/typescript/commonjs/core/session.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/core/theme.d.ts +43 -0
  134. package/lib/typescript/commonjs/core/theme.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/core/types.d.ts +185 -0
  136. package/lib/typescript/commonjs/core/types.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/core/websocket.d.ts +17 -0
  138. package/lib/typescript/commonjs/core/websocket.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/hooks/useChat.d.ts +35 -0
  140. package/lib/typescript/commonjs/hooks/useChat.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/hooks/useParlr.d.ts +11 -0
  142. package/lib/typescript/commonjs/hooks/useParlr.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/index.d.ts +30 -0
  144. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/package.json +1 -0
  146. package/lib/typescript/commonjs/provider/ParlrContext.d.ts +13 -0
  147. package/lib/typescript/commonjs/provider/ParlrContext.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts +5 -0
  149. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts.map +1 -0
  150. package/lib/typescript/module/components/AttachmentPicker.d.ts +23 -0
  151. package/lib/typescript/module/components/AttachmentPicker.d.ts.map +1 -0
  152. package/lib/typescript/module/components/AttachmentPreview.d.ts +16 -0
  153. package/lib/typescript/module/components/AttachmentPreview.d.ts.map +1 -0
  154. package/lib/typescript/module/components/ChatBubble.d.ts +14 -0
  155. package/lib/typescript/module/components/ChatBubble.d.ts.map +1 -0
  156. package/lib/typescript/module/components/EmptyState.d.ts +10 -0
  157. package/lib/typescript/module/components/EmptyState.d.ts.map +1 -0
  158. package/lib/typescript/module/components/ParlrChat.d.ts +34 -0
  159. package/lib/typescript/module/components/ParlrChat.d.ts.map +1 -0
  160. package/lib/typescript/module/components/ParlrConversationList.d.ts +17 -0
  161. package/lib/typescript/module/components/ParlrConversationList.d.ts.map +1 -0
  162. package/lib/typescript/module/components/PreChatForm.d.ts +20 -0
  163. package/lib/typescript/module/components/PreChatForm.d.ts.map +1 -0
  164. package/lib/typescript/module/components/RichMessage.d.ts +41 -0
  165. package/lib/typescript/module/components/RichMessage.d.ts.map +1 -0
  166. package/lib/typescript/module/components/SatisfactionSurvey.d.ts +17 -0
  167. package/lib/typescript/module/components/SatisfactionSurvey.d.ts.map +1 -0
  168. package/lib/typescript/module/components/TypingIndicator.d.ts +7 -0
  169. package/lib/typescript/module/components/TypingIndicator.d.ts.map +1 -0
  170. package/lib/typescript/module/core/api.d.ts +37 -0
  171. package/lib/typescript/module/core/api.d.ts.map +1 -0
  172. package/lib/typescript/module/core/config.d.ts +9 -0
  173. package/lib/typescript/module/core/config.d.ts.map +1 -0
  174. package/lib/typescript/module/core/errors.d.ts +35 -0
  175. package/lib/typescript/module/core/errors.d.ts.map +1 -0
  176. package/lib/typescript/module/core/offlineQueue.d.ts +16 -0
  177. package/lib/typescript/module/core/offlineQueue.d.ts.map +1 -0
  178. package/lib/typescript/module/core/pushNotifications.d.ts +6 -0
  179. package/lib/typescript/module/core/pushNotifications.d.ts.map +1 -0
  180. package/lib/typescript/module/core/session.d.ts +15 -0
  181. package/lib/typescript/module/core/session.d.ts.map +1 -0
  182. package/lib/typescript/module/core/theme.d.ts +43 -0
  183. package/lib/typescript/module/core/theme.d.ts.map +1 -0
  184. package/lib/typescript/module/core/types.d.ts +185 -0
  185. package/lib/typescript/module/core/types.d.ts.map +1 -0
  186. package/lib/typescript/module/core/websocket.d.ts +17 -0
  187. package/lib/typescript/module/core/websocket.d.ts.map +1 -0
  188. package/lib/typescript/module/hooks/useChat.d.ts +35 -0
  189. package/lib/typescript/module/hooks/useChat.d.ts.map +1 -0
  190. package/lib/typescript/module/hooks/useParlr.d.ts +11 -0
  191. package/lib/typescript/module/hooks/useParlr.d.ts.map +1 -0
  192. package/lib/typescript/module/index.d.ts +30 -0
  193. package/lib/typescript/module/index.d.ts.map +1 -0
  194. package/lib/typescript/module/package.json +1 -0
  195. package/lib/typescript/module/provider/ParlrContext.d.ts +13 -0
  196. package/lib/typescript/module/provider/ParlrContext.d.ts.map +1 -0
  197. package/lib/typescript/module/provider/ParlrProvider.d.ts +5 -0
  198. package/lib/typescript/module/provider/ParlrProvider.d.ts.map +1 -0
  199. package/package.json +120 -0
  200. package/src/components/AttachmentPicker.tsx +310 -0
  201. package/src/components/AttachmentPreview.tsx +209 -0
  202. package/src/components/ChatBubble.tsx +424 -0
  203. package/src/components/EmptyState.tsx +118 -0
  204. package/src/components/ParlrChat.tsx +863 -0
  205. package/src/components/ParlrConversationList.tsx +559 -0
  206. package/src/components/PreChatForm.tsx +313 -0
  207. package/src/components/RichMessage.tsx +353 -0
  208. package/src/components/SatisfactionSurvey.tsx +333 -0
  209. package/src/components/TypingIndicator.tsx +89 -0
  210. package/src/core/api.ts +406 -0
  211. package/src/core/config.ts +39 -0
  212. package/src/core/errors.ts +68 -0
  213. package/src/core/offlineQueue.ts +94 -0
  214. package/src/core/pushNotifications.ts +22 -0
  215. package/src/core/session.ts +156 -0
  216. package/src/core/theme.ts +133 -0
  217. package/src/core/types.ts +237 -0
  218. package/src/core/websocket.ts +270 -0
  219. package/src/hooks/useChat.ts +534 -0
  220. package/src/hooks/useParlr.ts +43 -0
  221. package/src/index.ts +98 -0
  222. package/src/provider/ParlrContext.ts +40 -0
  223. package/src/provider/ParlrProvider.tsx +338 -0
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@parlr/react-native",
3
+ "version": "0.1.0",
4
+ "description": "Official Parlr live chat SDK for React Native",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "types": "lib/typescript/commonjs/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "source": "src/index.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./lib/typescript/module/index.d.ts",
14
+ "default": "./lib/module/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./lib/typescript/commonjs/index.d.ts",
18
+ "default": "./lib/commonjs/index.js"
19
+ },
20
+ "react-native": "./src/index.ts",
21
+ "source": "./src/index.ts"
22
+ }
23
+ },
24
+ "files": [
25
+ "src",
26
+ "lib",
27
+ "!**/__tests__",
28
+ "!**/__fixtures__",
29
+ "!**/__mocks__"
30
+ ],
31
+ "scripts": {
32
+ "build": "bob build",
33
+ "typecheck": "tsc --noEmit",
34
+ "lint": "eslint src/ --ext .ts,.tsx",
35
+ "test": "jest",
36
+ "test:coverage": "jest --coverage",
37
+ "clean": "rm -rf lib/",
38
+ "prepublishOnly": "npm run clean && npm run build"
39
+ },
40
+ "peerDependencies": {
41
+ "expo-document-picker": ">=12.0.0",
42
+ "expo-image-picker": ">=15.0.0",
43
+ "expo-secure-store": ">=13.0.0",
44
+ "react": ">=18.0.0",
45
+ "react-native": ">=0.72.0",
46
+ "react-native-reanimated": ">=3.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "expo-document-picker": {
50
+ "optional": true
51
+ },
52
+ "expo-image-picker": {
53
+ "optional": true
54
+ },
55
+ "expo-secure-store": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "dependencies": {
60
+ "axios": "^1.7.0"
61
+ },
62
+ "devDependencies": {
63
+ "@testing-library/react-native": "^13.0.0",
64
+ "@types/react": "~19.1.0",
65
+ "@types/react-native": "~0.73.0",
66
+ "eslint": "^9.0.0",
67
+ "jest": "~29.7.0",
68
+ "prettier": "^3.7.0",
69
+ "react": "^18.3.0",
70
+ "react-native": "^0.76.0",
71
+ "react-native-builder-bob": "^0.40.0",
72
+ "react-native-reanimated": "^3.16.0",
73
+ "react-test-renderer": "^18.3.1",
74
+ "ts-jest": "^29.2.0",
75
+ "typescript": "~5.7.0"
76
+ },
77
+ "react-native-builder-bob": {
78
+ "source": "src",
79
+ "output": "lib",
80
+ "targets": [
81
+ [
82
+ "commonjs",
83
+ {
84
+ "esm": true
85
+ }
86
+ ],
87
+ [
88
+ "module",
89
+ {
90
+ "esm": true
91
+ }
92
+ ],
93
+ "typescript"
94
+ ]
95
+ },
96
+ "keywords": [
97
+ "parlr",
98
+ "chat",
99
+ "live-chat",
100
+ "react-native",
101
+ "support",
102
+ "customer-support",
103
+ "messaging"
104
+ ],
105
+ "engines": {
106
+ "node": ">=18.0.0"
107
+ },
108
+ "homepage": "https://parlr.chat/docs/sdk/react-native",
109
+ "bugs": "https://github.com/parlr/parlr/issues",
110
+ "repository": {
111
+ "type": "git",
112
+ "url": "https://github.com/parlr/parlr",
113
+ "directory": "parlr-react-native"
114
+ },
115
+ "license": "MIT",
116
+ "publishConfig": {
117
+ "access": "public",
118
+ "registry": "https://registry.npmjs.org/"
119
+ }
120
+ }
@@ -0,0 +1,310 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Attachment Picker
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Provides image and file picking capabilities for chat attachments.
6
+ // Uses expo-image-picker and expo-document-picker when available.
7
+ // Falls back gracefully with a user-facing alert if not installed.
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import React, { useCallback, useContext } from 'react';
11
+ import {
12
+ Alert,
13
+ Pressable,
14
+ StyleSheet,
15
+ Text,
16
+ View,
17
+ } from 'react-native';
18
+ import { ParlrContext } from '../provider/ParlrContext';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface PickedFile {
25
+ uri: string;
26
+ name: string;
27
+ type: string;
28
+ size: number;
29
+ }
30
+
31
+ export interface AttachmentPickerProps {
32
+ /** Called when a file is selected. */
33
+ onFilePicked: (file: PickedFile) => void;
34
+ /** Called to dismiss the picker. */
35
+ onDismiss: () => void;
36
+ /** Max file size in bytes. Default: 25MB. */
37
+ maxFileSize?: number;
38
+ /** Allowed MIME types. Default: images + documents. */
39
+ allowedTypes?: string[];
40
+ /** Accent color. */
41
+ accentColor?: string;
42
+ /** Locale. */
43
+ locale?: string;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Constants
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const MAX_FILE_SIZE_DEFAULT = 25 * 1024 * 1024; // 25 MB
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // i18n
54
+ // ---------------------------------------------------------------------------
55
+
56
+ function getI18n(locale?: string) {
57
+ if (locale?.startsWith('en')) {
58
+ return {
59
+ photo: 'Photo',
60
+ camera: 'Camera',
61
+ file: 'File',
62
+ cancel: 'Cancel',
63
+ tooLarge: 'File is too large (max 25 MB)',
64
+ permissionDenied: 'Permission denied',
65
+ permissionMessage: 'Please allow access in your device settings.',
66
+ missingLib: 'Feature unavailable',
67
+ missingLibMessage: 'The required library is not installed.',
68
+ ok: 'OK',
69
+ };
70
+ }
71
+ return {
72
+ photo: 'Photo',
73
+ camera: 'Cam\u00e9ra',
74
+ file: 'Fichier',
75
+ cancel: 'Annuler',
76
+ tooLarge: 'Fichier trop volumineux (max 25 Mo)',
77
+ permissionDenied: 'Permission refus\u00e9e',
78
+ permissionMessage: 'Veuillez autoriser l\u2019acc\u00e8s dans les r\u00e9glages.',
79
+ missingLib: 'Fonctionnalit\u00e9 indisponible',
80
+ missingLibMessage: 'La librairie requise n\u2019est pas install\u00e9e.',
81
+ ok: 'OK',
82
+ };
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Dynamic imports (these packages are optional peer dependencies)
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /* eslint-disable @typescript-eslint/no-explicit-any */
90
+ let ImagePicker: any = null;
91
+ let DocumentPicker: any = null;
92
+ /* eslint-enable @typescript-eslint/no-explicit-any */
93
+
94
+ try {
95
+ ImagePicker = require('expo-image-picker');
96
+ } catch {
97
+ // expo-image-picker not installed
98
+ }
99
+
100
+ try {
101
+ DocumentPicker = require('expo-document-picker');
102
+ } catch {
103
+ // expo-document-picker not installed
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Component
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export function AttachmentPicker({
111
+ onFilePicked,
112
+ onDismiss,
113
+ maxFileSize = MAX_FILE_SIZE_DEFAULT,
114
+ accentColor: accentColorProp,
115
+ locale,
116
+ }: AttachmentPickerProps) {
117
+ const { theme } = useContext(ParlrContext);
118
+ const accentColor = accentColorProp ?? theme.colors.primary;
119
+ const i18n = getI18n(locale);
120
+
121
+ const checkFileSize = useCallback(
122
+ (size: number): boolean => {
123
+ if (size > maxFileSize) {
124
+ Alert.alert(i18n.tooLarge);
125
+ return false;
126
+ }
127
+ return true;
128
+ },
129
+ [maxFileSize, i18n.tooLarge],
130
+ );
131
+
132
+ const handlePickPhoto = useCallback(async () => {
133
+ if (!ImagePicker) {
134
+ Alert.alert(i18n.missingLib, i18n.missingLibMessage);
135
+ return;
136
+ }
137
+
138
+ const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
139
+ if (!permission.granted) {
140
+ Alert.alert(i18n.permissionDenied, i18n.permissionMessage);
141
+ return;
142
+ }
143
+
144
+ const result = await ImagePicker.launchImageLibraryAsync({
145
+ mediaTypes: ['images'],
146
+ quality: 0.8,
147
+ allowsEditing: false,
148
+ });
149
+
150
+ if (result.canceled || !result.assets?.[0]) return;
151
+
152
+ const asset = result.assets[0];
153
+ const size = asset.fileSize ?? 0;
154
+ if (!checkFileSize(size)) return;
155
+
156
+ onFilePicked({
157
+ uri: asset.uri,
158
+ name: asset.fileName ?? `photo_${Date.now()}.jpg`,
159
+ type: asset.mimeType ?? 'image/jpeg',
160
+ size,
161
+ });
162
+ }, [onFilePicked, checkFileSize, i18n]);
163
+
164
+ const handlePickCamera = useCallback(async () => {
165
+ if (!ImagePicker) {
166
+ Alert.alert(i18n.missingLib, i18n.missingLibMessage);
167
+ return;
168
+ }
169
+
170
+ const permission = await ImagePicker.requestCameraPermissionsAsync();
171
+ if (!permission.granted) {
172
+ Alert.alert(i18n.permissionDenied, i18n.permissionMessage);
173
+ return;
174
+ }
175
+
176
+ const result = await ImagePicker.launchCameraAsync({
177
+ mediaTypes: ['images'],
178
+ quality: 0.8,
179
+ allowsEditing: false,
180
+ });
181
+
182
+ if (result.canceled || !result.assets?.[0]) return;
183
+
184
+ const asset = result.assets[0];
185
+ const size = asset.fileSize ?? 0;
186
+ if (!checkFileSize(size)) return;
187
+
188
+ onFilePicked({
189
+ uri: asset.uri,
190
+ name: asset.fileName ?? `camera_${Date.now()}.jpg`,
191
+ type: asset.mimeType ?? 'image/jpeg',
192
+ size,
193
+ });
194
+ }, [onFilePicked, checkFileSize, i18n]);
195
+
196
+ const handlePickFile = useCallback(async () => {
197
+ if (!DocumentPicker) {
198
+ Alert.alert(i18n.missingLib, i18n.missingLibMessage);
199
+ return;
200
+ }
201
+
202
+ const result = await DocumentPicker.getDocumentAsync({
203
+ copyToCacheDirectory: true,
204
+ });
205
+
206
+ if (result.canceled || !result.assets?.[0]) return;
207
+
208
+ const asset = result.assets[0];
209
+ const size = asset.size ?? 0;
210
+ if (!checkFileSize(size)) return;
211
+
212
+ onFilePicked({
213
+ uri: asset.uri,
214
+ name: asset.name ?? `file_${Date.now()}`,
215
+ type: asset.mimeType ?? 'application/octet-stream',
216
+ size,
217
+ });
218
+ }, [onFilePicked, checkFileSize, i18n]);
219
+
220
+ return (
221
+ <View style={[styles.root, { backgroundColor: theme.colors.surface, borderTopColor: theme.colors.border }]}>
222
+ <View style={styles.optionsRow}>
223
+ <Pressable
224
+ onPress={handlePickPhoto}
225
+ style={[styles.option, { backgroundColor: accentColor + '15' }]}
226
+ accessibilityRole="button"
227
+ accessibilityLabel={i18n.photo}
228
+ >
229
+ <Text style={[styles.optionIcon, { color: accentColor }]}>{'\ud83d\uddbc'}</Text>
230
+ <Text style={[styles.optionLabel, { color: theme.colors.textSecondary }]}>
231
+ {i18n.photo}
232
+ </Text>
233
+ </Pressable>
234
+
235
+ <Pressable
236
+ onPress={handlePickCamera}
237
+ style={[styles.option, { backgroundColor: accentColor + '15' }]}
238
+ accessibilityRole="button"
239
+ accessibilityLabel={i18n.camera}
240
+ >
241
+ <Text style={[styles.optionIcon, { color: accentColor }]}>{'\ud83d\udcf7'}</Text>
242
+ <Text style={[styles.optionLabel, { color: theme.colors.textSecondary }]}>
243
+ {i18n.camera}
244
+ </Text>
245
+ </Pressable>
246
+
247
+ <Pressable
248
+ onPress={handlePickFile}
249
+ style={[styles.option, { backgroundColor: accentColor + '15' }]}
250
+ accessibilityRole="button"
251
+ accessibilityLabel={i18n.file}
252
+ >
253
+ <Text style={[styles.optionIcon, { color: accentColor }]}>{'\ud83d\udcc4'}</Text>
254
+ <Text style={[styles.optionLabel, { color: theme.colors.textSecondary }]}>
255
+ {i18n.file}
256
+ </Text>
257
+ </Pressable>
258
+ </View>
259
+
260
+ <Pressable
261
+ onPress={onDismiss}
262
+ style={styles.cancelButton}
263
+ accessibilityRole="button"
264
+ accessibilityLabel={i18n.cancel}
265
+ >
266
+ <Text style={[styles.cancelText, { color: theme.colors.textSecondary }]}>
267
+ {i18n.cancel}
268
+ </Text>
269
+ </Pressable>
270
+ </View>
271
+ );
272
+ }
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // Styles
276
+ // ---------------------------------------------------------------------------
277
+
278
+ const styles = StyleSheet.create({
279
+ root: {
280
+ paddingHorizontal: 16,
281
+ paddingVertical: 12,
282
+ borderTopWidth: StyleSheet.hairlineWidth,
283
+ },
284
+ optionsRow: {
285
+ flexDirection: 'row',
286
+ gap: 12,
287
+ marginBottom: 8,
288
+ },
289
+ option: {
290
+ flex: 1,
291
+ alignItems: 'center',
292
+ paddingVertical: 16,
293
+ borderRadius: 12,
294
+ },
295
+ optionIcon: {
296
+ fontSize: 24,
297
+ marginBottom: 4,
298
+ },
299
+ optionLabel: {
300
+ fontSize: 12,
301
+ fontWeight: '500',
302
+ },
303
+ cancelButton: {
304
+ alignItems: 'center',
305
+ paddingVertical: 8,
306
+ },
307
+ cancelText: {
308
+ fontSize: 14,
309
+ },
310
+ });
@@ -0,0 +1,209 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Attachment Preview
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Shows a preview of the selected attachment before sending it.
6
+ // Supports image thumbnails and file name/size display.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import React from 'react';
10
+ import {
11
+ Image,
12
+ Pressable,
13
+ StyleSheet,
14
+ Text,
15
+ View,
16
+ useColorScheme,
17
+ } from 'react-native';
18
+ import type { PickedFile } from './AttachmentPicker';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Props
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface AttachmentPreviewProps {
25
+ /** The file to preview. */
26
+ file: PickedFile;
27
+ /** Called when the user removes the attachment. */
28
+ onRemove: () => void;
29
+ /** Called when the user confirms and sends. */
30
+ onSend: () => void;
31
+ /** Accent color. */
32
+ accentColor?: string;
33
+ /** Locale. */
34
+ locale?: string;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function formatFileSize(bytes: number): string {
42
+ if (bytes < 1024) return `${bytes} B`;
43
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
44
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
45
+ }
46
+
47
+ function isImage(type: string): boolean {
48
+ return type.startsWith('image/');
49
+ }
50
+
51
+ function getI18n(locale?: string) {
52
+ if (locale?.startsWith('en')) {
53
+ return { send: 'Send', remove: 'Remove' };
54
+ }
55
+ return { send: 'Envoyer', remove: 'Supprimer' };
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Component
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export function AttachmentPreview({
63
+ file,
64
+ onRemove,
65
+ onSend,
66
+ accentColor: accentColorProp,
67
+ locale,
68
+ }: AttachmentPreviewProps) {
69
+ const isDark = useColorScheme() === 'dark';
70
+ const accentColor = accentColorProp ?? '#6366f1';
71
+ const i18n = getI18n(locale);
72
+
73
+ return (
74
+ <View style={[styles.root, isDark && styles.rootDark]}>
75
+ <View style={[styles.previewCard, isDark && styles.previewCardDark]}>
76
+ {isImage(file.type) ? (
77
+ <Image
78
+ source={{ uri: file.uri }}
79
+ style={styles.imagePreview}
80
+ resizeMode="cover"
81
+ accessibilityLabel={file.name}
82
+ />
83
+ ) : (
84
+ <View style={styles.filePreview}>
85
+ <Text style={styles.fileIcon}>{'\ud83d\udcc4'}</Text>
86
+ <View style={styles.fileInfo}>
87
+ <Text
88
+ style={[styles.fileName, isDark && styles.fileNameDark]}
89
+ numberOfLines={1}
90
+ >
91
+ {file.name}
92
+ </Text>
93
+ <Text style={[styles.fileSize, isDark && styles.fileSizeDark]}>
94
+ {formatFileSize(file.size)}
95
+ </Text>
96
+ </View>
97
+ </View>
98
+ )}
99
+
100
+ {/* Remove button */}
101
+ <Pressable
102
+ onPress={onRemove}
103
+ style={styles.removeButton}
104
+ hitSlop={8}
105
+ accessibilityRole="button"
106
+ accessibilityLabel={i18n.remove}
107
+ >
108
+ <Text style={styles.removeIcon}>{'\u2715'}</Text>
109
+ </Pressable>
110
+ </View>
111
+
112
+ {/* Send button */}
113
+ <Pressable
114
+ onPress={onSend}
115
+ style={[styles.sendButton, { backgroundColor: accentColor }]}
116
+ accessibilityRole="button"
117
+ accessibilityLabel={i18n.send}
118
+ >
119
+ <Text style={styles.sendText}>{i18n.send}</Text>
120
+ </Pressable>
121
+ </View>
122
+ );
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Styles
127
+ // ---------------------------------------------------------------------------
128
+
129
+ const styles = StyleSheet.create({
130
+ root: {
131
+ backgroundColor: '#ffffff',
132
+ padding: 12,
133
+ borderTopWidth: StyleSheet.hairlineWidth,
134
+ borderTopColor: '#e2e8f0',
135
+ },
136
+ rootDark: {
137
+ backgroundColor: '#0f172a',
138
+ borderTopColor: '#1e293b',
139
+ },
140
+ previewCard: {
141
+ backgroundColor: '#f8fafc',
142
+ borderRadius: 12,
143
+ overflow: 'hidden',
144
+ marginBottom: 10,
145
+ position: 'relative',
146
+ },
147
+ previewCardDark: {
148
+ backgroundColor: '#1e293b',
149
+ },
150
+ imagePreview: {
151
+ width: '100%',
152
+ height: 200,
153
+ borderRadius: 12,
154
+ },
155
+ filePreview: {
156
+ flexDirection: 'row',
157
+ alignItems: 'center',
158
+ padding: 14,
159
+ },
160
+ fileIcon: {
161
+ fontSize: 32,
162
+ marginRight: 12,
163
+ },
164
+ fileInfo: {
165
+ flex: 1,
166
+ },
167
+ fileName: {
168
+ fontSize: 14,
169
+ fontWeight: '500',
170
+ color: '#0f172a',
171
+ marginBottom: 2,
172
+ },
173
+ fileNameDark: {
174
+ color: '#f1f5f9',
175
+ },
176
+ fileSize: {
177
+ fontSize: 12,
178
+ color: '#64748b',
179
+ },
180
+ fileSizeDark: {
181
+ color: '#94a3b8',
182
+ },
183
+ removeButton: {
184
+ position: 'absolute',
185
+ top: 8,
186
+ right: 8,
187
+ width: 28,
188
+ height: 28,
189
+ borderRadius: 14,
190
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
191
+ justifyContent: 'center',
192
+ alignItems: 'center',
193
+ },
194
+ removeIcon: {
195
+ color: '#ffffff',
196
+ fontSize: 14,
197
+ fontWeight: '700',
198
+ },
199
+ sendButton: {
200
+ paddingVertical: 12,
201
+ borderRadius: 10,
202
+ alignItems: 'center',
203
+ },
204
+ sendText: {
205
+ color: '#ffffff',
206
+ fontSize: 15,
207
+ fontWeight: '600',
208
+ },
209
+ });