@harkenapp/sdk-react-native 0.0.1-alpha.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.
- package/README.md +67 -0
- package/app.plugin.cjs +135 -0
- package/app.plugin.js +1 -0
- package/dist/api/client.d.ts +67 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +163 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/errors.d.ts +46 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +72 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +20 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/retry.d.ts +29 -0
- package/dist/api/retry.d.ts.map +1 -0
- package/dist/api/retry.js +74 -0
- package/dist/api/retry.js.map +1 -0
- package/dist/attachments/FeedbackSheet.d.ts +88 -0
- package/dist/attachments/FeedbackSheet.d.ts.map +1 -0
- package/dist/attachments/FeedbackSheet.js +250 -0
- package/dist/attachments/FeedbackSheet.js.map +1 -0
- package/dist/attachments/index.d.ts +20 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +40 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/components/AttachmentGrid.d.ts +94 -0
- package/dist/components/AttachmentGrid.d.ts.map +1 -0
- package/dist/components/AttachmentGrid.js +132 -0
- package/dist/components/AttachmentGrid.js.map +1 -0
- package/dist/components/AttachmentPicker.d.ts +98 -0
- package/dist/components/AttachmentPicker.d.ts.map +1 -0
- package/dist/components/AttachmentPicker.js +297 -0
- package/dist/components/AttachmentPicker.js.map +1 -0
- package/dist/components/AttachmentPreview.d.ts +78 -0
- package/dist/components/AttachmentPreview.d.ts.map +1 -0
- package/dist/components/AttachmentPreview.js +133 -0
- package/dist/components/AttachmentPreview.js.map +1 -0
- package/dist/components/CategorySelector.d.ts +77 -0
- package/dist/components/CategorySelector.d.ts.map +1 -0
- package/dist/components/CategorySelector.js +117 -0
- package/dist/components/CategorySelector.js.map +1 -0
- package/dist/components/FeedbackForm.d.ts +50 -0
- package/dist/components/FeedbackForm.d.ts.map +1 -0
- package/dist/components/FeedbackForm.js +141 -0
- package/dist/components/FeedbackForm.js.map +1 -0
- package/dist/components/FeedbackSheet.d.ts +75 -0
- package/dist/components/FeedbackSheet.d.ts.map +1 -0
- package/dist/components/FeedbackSheet.js +215 -0
- package/dist/components/FeedbackSheet.js.map +1 -0
- package/dist/components/ThemedButton.d.ts +23 -0
- package/dist/components/ThemedButton.d.ts.map +1 -0
- package/dist/components/ThemedButton.js +77 -0
- package/dist/components/ThemedButton.js.map +1 -0
- package/dist/components/ThemedText.d.ts +16 -0
- package/dist/components/ThemedText.d.ts.map +1 -0
- package/dist/components/ThemedText.js +44 -0
- package/dist/components/ThemedText.js.map +1 -0
- package/dist/components/ThemedTextInput.d.ts +13 -0
- package/dist/components/ThemedTextInput.d.ts.map +1 -0
- package/dist/components/ThemedTextInput.js +76 -0
- package/dist/components/ThemedTextInput.js.map +1 -0
- package/dist/components/UploadStatusOverlay.d.ts +82 -0
- package/dist/components/UploadStatusOverlay.d.ts.map +1 -0
- package/dist/components/UploadStatusOverlay.js +319 -0
- package/dist/components/UploadStatusOverlay.js.map +1 -0
- package/dist/components/index.d.ts +19 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +28 -0
- package/dist/components/index.js.map +1 -0
- package/dist/context/HarkenContext.d.ts +62 -0
- package/dist/context/HarkenContext.d.ts.map +1 -0
- package/dist/context/HarkenContext.js +128 -0
- package/dist/context/HarkenContext.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +7 -0
- package/dist/context/index.js.map +1 -0
- package/dist/domain/index.d.ts +3 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +7 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/upload-queue.d.ts +116 -0
- package/dist/domain/upload-queue.d.ts.map +1 -0
- package/dist/domain/upload-queue.js +34 -0
- package/dist/domain/upload-queue.js.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +16 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useAnonymousId.d.ts +28 -0
- package/dist/hooks/useAnonymousId.d.ts.map +1 -0
- package/dist/hooks/useAnonymousId.js +59 -0
- package/dist/hooks/useAnonymousId.js.map +1 -0
- package/dist/hooks/useAttachmentPicker.d.ts +84 -0
- package/dist/hooks/useAttachmentPicker.d.ts.map +1 -0
- package/dist/hooks/useAttachmentPicker.js +181 -0
- package/dist/hooks/useAttachmentPicker.js.map +1 -0
- package/dist/hooks/useAttachmentStatus.d.ts +51 -0
- package/dist/hooks/useAttachmentStatus.d.ts.map +1 -0
- package/dist/hooks/useAttachmentStatus.js +69 -0
- package/dist/hooks/useAttachmentStatus.js.map +1 -0
- package/dist/hooks/useAttachmentUpload.d.ts +101 -0
- package/dist/hooks/useAttachmentUpload.d.ts.map +1 -0
- package/dist/hooks/useAttachmentUpload.js +293 -0
- package/dist/hooks/useAttachmentUpload.js.map +1 -0
- package/dist/hooks/useFeedback.d.ts +55 -0
- package/dist/hooks/useFeedback.d.ts.map +1 -0
- package/dist/hooks/useFeedback.js +96 -0
- package/dist/hooks/useFeedback.js.map +1 -0
- package/dist/hooks/useHarkenContext.d.ts +25 -0
- package/dist/hooks/useHarkenContext.d.ts.map +1 -0
- package/dist/hooks/useHarkenContext.js +35 -0
- package/dist/hooks/useHarkenContext.js.map +1 -0
- package/dist/hooks/useHarkenTheme.d.ts +26 -0
- package/dist/hooks/useHarkenTheme.d.ts.map +1 -0
- package/dist/hooks/useHarkenTheme.js +36 -0
- package/dist/hooks/useHarkenTheme.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +9 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/uploadQueueService.d.ts +193 -0
- package/dist/services/uploadQueueService.d.ts.map +1 -0
- package/dist/services/uploadQueueService.js +623 -0
- package/dist/services/uploadQueueService.js.map +1 -0
- package/dist/services/uploadQueueStorage.d.ts +30 -0
- package/dist/services/uploadQueueStorage.d.ts.map +1 -0
- package/dist/services/uploadQueueStorage.js +77 -0
- package/dist/services/uploadQueueStorage.js.map +1 -0
- package/dist/storage/IdentityStore.d.ts +38 -0
- package/dist/storage/IdentityStore.d.ts.map +1 -0
- package/dist/storage/IdentityStore.js +83 -0
- package/dist/storage/IdentityStore.js.map +1 -0
- package/dist/storage/SecureStoreAdapter.d.ts +28 -0
- package/dist/storage/SecureStoreAdapter.d.ts.map +1 -0
- package/dist/storage/SecureStoreAdapter.js +52 -0
- package/dist/storage/SecureStoreAdapter.js.map +1 -0
- package/dist/storage/defaultStorage.d.ts +20 -0
- package/dist/storage/defaultStorage.d.ts.map +1 -0
- package/dist/storage/defaultStorage.js +131 -0
- package/dist/storage/defaultStorage.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/types.d.ts +32 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +11 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/theme/defaults.d.ts +43 -0
- package/dist/theme/defaults.d.ts.map +1 -0
- package/dist/theme/defaults.js +128 -0
- package/dist/theme/defaults.js.map +1 -0
- package/dist/theme/index.d.ts +3 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +14 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/types.d.ts +136 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +3 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/types/config.d.ts +100 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/openapi.d.ts +601 -0
- package/dist/types/openapi.d.ts.map +1 -0
- package/dist/types/openapi.js +7 -0
- package/dist/types/openapi.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/uuid.d.ts +10 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/dist/utils/uuid.js +60 -0
- package/dist/utils/uuid.js.map +1 -0
- package/package.json +124 -0
- package/src/@types/expo-file-system-legacy.d.ts +13 -0
- package/src/api/client.ts +250 -0
- package/src/api/errors.ts +84 -0
- package/src/api/index.ts +15 -0
- package/src/api/retry.ts +99 -0
- package/src/attachments/FeedbackSheet.tsx +400 -0
- package/src/attachments/index.ts +70 -0
- package/src/components/AttachmentGrid.tsx +247 -0
- package/src/components/AttachmentPicker.tsx +391 -0
- package/src/components/AttachmentPreview.tsx +210 -0
- package/src/components/CategorySelector.tsx +174 -0
- package/src/components/FeedbackForm.tsx +216 -0
- package/src/components/FeedbackSheet.tsx +321 -0
- package/src/components/ThemedButton.tsx +127 -0
- package/src/components/ThemedText.tsx +65 -0
- package/src/components/ThemedTextInput.tsx +65 -0
- package/src/components/UploadStatusOverlay.tsx +440 -0
- package/src/components/index.ts +39 -0
- package/src/context/HarkenContext.tsx +129 -0
- package/src/context/index.ts +2 -0
- package/src/domain/index.ts +12 -0
- package/src/domain/upload-queue.ts +131 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useAnonymousId.ts +68 -0
- package/src/hooks/useAttachmentPicker.ts +243 -0
- package/src/hooks/useAttachmentStatus.ts +86 -0
- package/src/hooks/useAttachmentUpload.ts +370 -0
- package/src/hooks/useFeedback.ts +139 -0
- package/src/hooks/useHarkenContext.ts +35 -0
- package/src/hooks/useHarkenTheme.ts +36 -0
- package/src/index.ts +168 -0
- package/src/services/index.ts +11 -0
- package/src/services/uploadQueueService.ts +727 -0
- package/src/services/uploadQueueStorage.ts +78 -0
- package/src/storage/IdentityStore.ts +89 -0
- package/src/storage/SecureStoreAdapter.ts +59 -0
- package/src/storage/defaultStorage.ts +109 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/types.ts +34 -0
- package/src/theme/defaults.ts +151 -0
- package/src/theme/index.ts +23 -0
- package/src/theme/types.ts +157 -0
- package/src/types/config.ts +112 -0
- package/src/types/index.ts +10 -0
- package/src/types/openapi.ts +601 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/uuid.ts +77 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Modal,
|
|
5
|
+
Pressable,
|
|
6
|
+
Dimensions,
|
|
7
|
+
Platform,
|
|
8
|
+
ActionSheetIOS,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import type { ViewStyle, StyleProp } from 'react-native';
|
|
12
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
13
|
+
import { useHarkenTheme } from '../hooks';
|
|
14
|
+
import { ThemedText } from './ThemedText';
|
|
15
|
+
|
|
16
|
+
export type AttachmentSource = 'camera' | 'library' | 'document';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for a single picker option.
|
|
20
|
+
*/
|
|
21
|
+
export interface PickerOptionConfig {
|
|
22
|
+
/** Custom label text */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Custom description text */
|
|
25
|
+
description?: string;
|
|
26
|
+
/** Icon background color (defaults to theme accent colors) */
|
|
27
|
+
color?: string;
|
|
28
|
+
/** Custom icon element (replaces default) */
|
|
29
|
+
icon?: React.ReactNode;
|
|
30
|
+
/** Hide this option entirely */
|
|
31
|
+
hidden?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AttachmentPickerProps {
|
|
35
|
+
/** Whether the picker is visible */
|
|
36
|
+
visible: boolean;
|
|
37
|
+
/** Callback when picker is closed */
|
|
38
|
+
onClose: () => void;
|
|
39
|
+
/** Callback when camera is selected */
|
|
40
|
+
onTakePhoto: () => void;
|
|
41
|
+
/** Callback when photo library is selected */
|
|
42
|
+
onPickFromLibrary: () => void;
|
|
43
|
+
/** Callback when document picker is selected */
|
|
44
|
+
onPickDocument: () => void;
|
|
45
|
+
/** Title shown in the picker */
|
|
46
|
+
title?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Custom render function for option icons.
|
|
49
|
+
* @deprecated Use `options.camera.icon` etc. instead
|
|
50
|
+
*/
|
|
51
|
+
renderIcon?: (source: AttachmentSource) => React.ReactNode;
|
|
52
|
+
/** Customize individual picker options */
|
|
53
|
+
options?: {
|
|
54
|
+
camera?: PickerOptionConfig;
|
|
55
|
+
library?: PickerOptionConfig;
|
|
56
|
+
document?: PickerOptionConfig;
|
|
57
|
+
};
|
|
58
|
+
/** Cancel button label */
|
|
59
|
+
cancelLabel?: string;
|
|
60
|
+
/** Overlay background color */
|
|
61
|
+
overlayColor?: string;
|
|
62
|
+
/** Bottom sheet corner radius */
|
|
63
|
+
sheetRadius?: number;
|
|
64
|
+
/** Additional style for bottom sheet container */
|
|
65
|
+
sheetStyle?: StyleProp<ViewStyle>;
|
|
66
|
+
/** Additional style for option rows */
|
|
67
|
+
optionStyle?: StyleProp<ViewStyle>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface PickerOption {
|
|
71
|
+
key: AttachmentSource;
|
|
72
|
+
label: string;
|
|
73
|
+
description: string;
|
|
74
|
+
color: string;
|
|
75
|
+
icon: React.ReactNode;
|
|
76
|
+
action: () => void;
|
|
77
|
+
hidden: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Platform-appropriate attachment source picker.
|
|
82
|
+
*
|
|
83
|
+
* - **iOS**: Uses native `ActionSheetIOS` for platform-native experience
|
|
84
|
+
* - **Android**: Uses a bottom sheet modal with styled options
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* // Basic usage
|
|
89
|
+
* <AttachmentPicker
|
|
90
|
+
* visible={showPicker}
|
|
91
|
+
* onClose={() => setShowPicker(false)}
|
|
92
|
+
* onTakePhoto={() => pickImage('camera')}
|
|
93
|
+
* onPickFromLibrary={() => pickImage('library')}
|
|
94
|
+
* onPickDocument={() => pickDocument()}
|
|
95
|
+
* />
|
|
96
|
+
*
|
|
97
|
+
* // With customization
|
|
98
|
+
* <AttachmentPicker
|
|
99
|
+
* visible={showPicker}
|
|
100
|
+
* onClose={() => setShowPicker(false)}
|
|
101
|
+
* onTakePhoto={() => pickImage('camera')}
|
|
102
|
+
* onPickFromLibrary={() => pickImage('library')}
|
|
103
|
+
* onPickDocument={() => pickDocument()}
|
|
104
|
+
* title="Attach File"
|
|
105
|
+
* cancelLabel="Dismiss"
|
|
106
|
+
* options={{
|
|
107
|
+
* camera: {
|
|
108
|
+
* label: 'Take Photo',
|
|
109
|
+
* icon: <CameraIcon />,
|
|
110
|
+
* color: '#007AFF',
|
|
111
|
+
* },
|
|
112
|
+
* library: {
|
|
113
|
+
* label: 'Choose Photo',
|
|
114
|
+
* icon: <PhotoIcon />,
|
|
115
|
+
* },
|
|
116
|
+
* document: {
|
|
117
|
+
* hidden: true, // Hide files option
|
|
118
|
+
* },
|
|
119
|
+
* }}
|
|
120
|
+
* />
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function AttachmentPicker({
|
|
124
|
+
visible,
|
|
125
|
+
onClose,
|
|
126
|
+
onTakePhoto,
|
|
127
|
+
onPickFromLibrary,
|
|
128
|
+
onPickDocument,
|
|
129
|
+
title = 'Add Attachment',
|
|
130
|
+
renderIcon,
|
|
131
|
+
options: optionOverrides,
|
|
132
|
+
cancelLabel = 'Cancel',
|
|
133
|
+
overlayColor,
|
|
134
|
+
sheetRadius,
|
|
135
|
+
sheetStyle,
|
|
136
|
+
optionStyle,
|
|
137
|
+
}: AttachmentPickerProps): React.JSX.Element | null {
|
|
138
|
+
const theme = useHarkenTheme();
|
|
139
|
+
const screenHeight = Dimensions.get('window').height;
|
|
140
|
+
|
|
141
|
+
// Prevent double-triggering ActionSheetIOS if callbacks change
|
|
142
|
+
const isShowingRef = useRef(false);
|
|
143
|
+
|
|
144
|
+
// Build options with defaults and overrides
|
|
145
|
+
const options: PickerOption[] = [
|
|
146
|
+
{
|
|
147
|
+
key: 'camera',
|
|
148
|
+
label: optionOverrides?.camera?.label ?? 'Camera',
|
|
149
|
+
description: optionOverrides?.camera?.description ?? 'Take a new photo',
|
|
150
|
+
color: optionOverrides?.camera?.color ?? theme.colors.accent1,
|
|
151
|
+
icon:
|
|
152
|
+
optionOverrides?.camera?.icon ??
|
|
153
|
+
(renderIcon ? renderIcon('camera') : <DefaultIcon emoji="📷" />),
|
|
154
|
+
action: onTakePhoto,
|
|
155
|
+
hidden: optionOverrides?.camera?.hidden ?? false,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'library',
|
|
159
|
+
label: optionOverrides?.library?.label ?? 'Photo Library',
|
|
160
|
+
description:
|
|
161
|
+
optionOverrides?.library?.description ?? 'Choose from existing photos',
|
|
162
|
+
color: optionOverrides?.library?.color ?? theme.colors.accent2,
|
|
163
|
+
icon:
|
|
164
|
+
optionOverrides?.library?.icon ??
|
|
165
|
+
(renderIcon ? renderIcon('library') : <DefaultIcon emoji="🖼️" />),
|
|
166
|
+
action: onPickFromLibrary,
|
|
167
|
+
hidden: optionOverrides?.library?.hidden ?? false,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: 'document',
|
|
171
|
+
label: optionOverrides?.document?.label ?? 'Files',
|
|
172
|
+
description:
|
|
173
|
+
optionOverrides?.document?.description ?? 'Browse documents and files',
|
|
174
|
+
color: optionOverrides?.document?.color ?? theme.colors.accent3,
|
|
175
|
+
icon:
|
|
176
|
+
optionOverrides?.document?.icon ??
|
|
177
|
+
(renderIcon ? renderIcon('document') : <DefaultIcon emoji="📄" />),
|
|
178
|
+
action: onPickDocument,
|
|
179
|
+
hidden: optionOverrides?.document?.hidden ?? false,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const visibleOptions = options.filter((o) => !o.hidden);
|
|
184
|
+
|
|
185
|
+
const handleOptionPress = (action: () => void) => {
|
|
186
|
+
onClose();
|
|
187
|
+
// Small delay to let modal close animation finish
|
|
188
|
+
setTimeout(action, 100);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// iOS: Use native ActionSheetIOS
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (!visible) {
|
|
194
|
+
isShowingRef.current = false;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (visible && Platform.OS === 'ios' && !isShowingRef.current) {
|
|
199
|
+
isShowingRef.current = true;
|
|
200
|
+
|
|
201
|
+
// Build iOS action sheet options from visible options
|
|
202
|
+
const iosOptions = [cancelLabel, ...visibleOptions.map((o) => o.label)];
|
|
203
|
+
|
|
204
|
+
ActionSheetIOS.showActionSheetWithOptions(
|
|
205
|
+
{
|
|
206
|
+
options: iosOptions,
|
|
207
|
+
cancelButtonIndex: 0,
|
|
208
|
+
title,
|
|
209
|
+
},
|
|
210
|
+
(buttonIndex) => {
|
|
211
|
+
isShowingRef.current = false;
|
|
212
|
+
onClose();
|
|
213
|
+
if (buttonIndex > 0) {
|
|
214
|
+
const selectedOption = visibleOptions[buttonIndex - 1];
|
|
215
|
+
if (selectedOption) {
|
|
216
|
+
setTimeout(() => selectedOption.action(), 100);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}, [visible, onClose, visibleOptions, title, cancelLabel]);
|
|
223
|
+
|
|
224
|
+
// iOS: Don't render modal - we use ActionSheetIOS instead
|
|
225
|
+
if (Platform.OS === 'ios') {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const resolvedOverlayColor = overlayColor ?? theme.colors.overlay;
|
|
230
|
+
const resolvedSheetRadius = sheetRadius ?? theme.radii.xl;
|
|
231
|
+
|
|
232
|
+
// Android: Use bottom sheet modal
|
|
233
|
+
return (
|
|
234
|
+
<Modal
|
|
235
|
+
visible={visible}
|
|
236
|
+
transparent
|
|
237
|
+
animationType="slide"
|
|
238
|
+
onRequestClose={onClose}
|
|
239
|
+
>
|
|
240
|
+
<SafeAreaView style={styles.modalContainer}>
|
|
241
|
+
{/* Background overlay */}
|
|
242
|
+
<Pressable
|
|
243
|
+
style={[styles.overlay, { backgroundColor: resolvedOverlayColor }]}
|
|
244
|
+
onPress={onClose}
|
|
245
|
+
>
|
|
246
|
+
{/* Bottom sheet */}
|
|
247
|
+
<View
|
|
248
|
+
style={[
|
|
249
|
+
styles.bottomSheet,
|
|
250
|
+
{
|
|
251
|
+
backgroundColor: theme.colors.background,
|
|
252
|
+
maxHeight: screenHeight * 0.6,
|
|
253
|
+
borderTopLeftRadius: resolvedSheetRadius,
|
|
254
|
+
borderTopRightRadius: resolvedSheetRadius,
|
|
255
|
+
},
|
|
256
|
+
sheetStyle,
|
|
257
|
+
]}
|
|
258
|
+
// Prevent touches from passing through to background
|
|
259
|
+
onStartShouldSetResponder={() => true}
|
|
260
|
+
>
|
|
261
|
+
{/* Handle bar */}
|
|
262
|
+
<View style={styles.handleContainer}>
|
|
263
|
+
<View
|
|
264
|
+
style={[
|
|
265
|
+
styles.handle,
|
|
266
|
+
{ backgroundColor: theme.colors.textSecondary },
|
|
267
|
+
]}
|
|
268
|
+
/>
|
|
269
|
+
</View>
|
|
270
|
+
|
|
271
|
+
{/* Title */}
|
|
272
|
+
<View style={styles.titleContainer}>
|
|
273
|
+
<ThemedText variant="title" style={styles.title}>
|
|
274
|
+
{title}
|
|
275
|
+
</ThemedText>
|
|
276
|
+
</View>
|
|
277
|
+
|
|
278
|
+
{/* Options */}
|
|
279
|
+
<View style={styles.optionsContainer}>
|
|
280
|
+
{visibleOptions.map((option) => (
|
|
281
|
+
<Pressable
|
|
282
|
+
key={option.key}
|
|
283
|
+
style={({ pressed }) => [
|
|
284
|
+
styles.option,
|
|
285
|
+
{
|
|
286
|
+
backgroundColor: pressed
|
|
287
|
+
? theme.colors.border
|
|
288
|
+
: theme.colors.backgroundSecondary,
|
|
289
|
+
borderRadius: theme.radii.md,
|
|
290
|
+
},
|
|
291
|
+
optionStyle,
|
|
292
|
+
]}
|
|
293
|
+
onPress={() => handleOptionPress(option.action)}
|
|
294
|
+
>
|
|
295
|
+
<View
|
|
296
|
+
style={[
|
|
297
|
+
styles.iconContainer,
|
|
298
|
+
{
|
|
299
|
+
backgroundColor: option.color,
|
|
300
|
+
borderRadius: theme.radii.full,
|
|
301
|
+
},
|
|
302
|
+
]}
|
|
303
|
+
>
|
|
304
|
+
{option.icon}
|
|
305
|
+
</View>
|
|
306
|
+
<View style={styles.optionText}>
|
|
307
|
+
<ThemedText variant="label">{option.label}</ThemedText>
|
|
308
|
+
<ThemedText variant="caption" secondary>
|
|
309
|
+
{option.description}
|
|
310
|
+
</ThemedText>
|
|
311
|
+
</View>
|
|
312
|
+
</Pressable>
|
|
313
|
+
))}
|
|
314
|
+
|
|
315
|
+
{/* Cancel Button */}
|
|
316
|
+
<Pressable style={styles.cancelButton} onPress={onClose}>
|
|
317
|
+
<ThemedText secondary>{cancelLabel}</ThemedText>
|
|
318
|
+
</Pressable>
|
|
319
|
+
</View>
|
|
320
|
+
</View>
|
|
321
|
+
</Pressable>
|
|
322
|
+
</SafeAreaView>
|
|
323
|
+
</Modal>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Default emoji icon component.
|
|
329
|
+
*/
|
|
330
|
+
function DefaultIcon({ emoji }: { emoji: string }): React.JSX.Element {
|
|
331
|
+
return <ThemedText style={styles.defaultIcon}>{emoji}</ThemedText>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const styles = StyleSheet.create({
|
|
335
|
+
modalContainer: {
|
|
336
|
+
flex: 1,
|
|
337
|
+
},
|
|
338
|
+
overlay: {
|
|
339
|
+
flex: 1,
|
|
340
|
+
justifyContent: 'flex-end',
|
|
341
|
+
},
|
|
342
|
+
bottomSheet: {
|
|
343
|
+
paddingBottom: 20,
|
|
344
|
+
},
|
|
345
|
+
handleContainer: {
|
|
346
|
+
alignItems: 'center',
|
|
347
|
+
paddingVertical: 12,
|
|
348
|
+
},
|
|
349
|
+
handle: {
|
|
350
|
+
width: 36,
|
|
351
|
+
height: 4,
|
|
352
|
+
borderRadius: 2,
|
|
353
|
+
opacity: 0.3,
|
|
354
|
+
},
|
|
355
|
+
titleContainer: {
|
|
356
|
+
paddingHorizontal: 20,
|
|
357
|
+
paddingBottom: 16,
|
|
358
|
+
},
|
|
359
|
+
title: {
|
|
360
|
+
textAlign: 'center',
|
|
361
|
+
},
|
|
362
|
+
optionsContainer: {
|
|
363
|
+
paddingHorizontal: 20,
|
|
364
|
+
},
|
|
365
|
+
option: {
|
|
366
|
+
flexDirection: 'row',
|
|
367
|
+
alignItems: 'center',
|
|
368
|
+
paddingVertical: 16,
|
|
369
|
+
paddingHorizontal: 16,
|
|
370
|
+
marginBottom: 8,
|
|
371
|
+
},
|
|
372
|
+
iconContainer: {
|
|
373
|
+
width: 44,
|
|
374
|
+
height: 44,
|
|
375
|
+
alignItems: 'center',
|
|
376
|
+
justifyContent: 'center',
|
|
377
|
+
marginRight: 16,
|
|
378
|
+
},
|
|
379
|
+
defaultIcon: {
|
|
380
|
+
fontSize: 22,
|
|
381
|
+
},
|
|
382
|
+
optionText: {
|
|
383
|
+
flex: 1,
|
|
384
|
+
gap: 2,
|
|
385
|
+
},
|
|
386
|
+
cancelButton: {
|
|
387
|
+
paddingVertical: 16,
|
|
388
|
+
alignItems: 'center',
|
|
389
|
+
marginTop: 12,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Image, StyleSheet } from 'react-native';
|
|
3
|
+
import type { ViewStyle, StyleProp, ImageStyle } from 'react-native';
|
|
4
|
+
import { useHarkenTheme } from '../hooks';
|
|
5
|
+
import { ThemedText } from './ThemedText';
|
|
6
|
+
import { UploadStatusOverlay } from './UploadStatusOverlay';
|
|
7
|
+
import type { UploadStatusLabels } from './UploadStatusOverlay';
|
|
8
|
+
import { UploadPhase } from '../domain';
|
|
9
|
+
|
|
10
|
+
export interface AttachmentPreviewProps {
|
|
11
|
+
/** Local file URI for preview */
|
|
12
|
+
uri: string;
|
|
13
|
+
/** MIME type of the file */
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
/** File name (shown for non-image files) */
|
|
16
|
+
fileName?: string;
|
|
17
|
+
/** Current upload phase */
|
|
18
|
+
phase: UploadPhase;
|
|
19
|
+
/** Upload progress (0.0 - 1.0) */
|
|
20
|
+
progress: number;
|
|
21
|
+
/** Error message if failed */
|
|
22
|
+
error?: string;
|
|
23
|
+
/** Callback when retry is pressed */
|
|
24
|
+
onRetry?: () => void;
|
|
25
|
+
/** Callback when remove is pressed */
|
|
26
|
+
onRemove?: () => void;
|
|
27
|
+
/** Size of the preview */
|
|
28
|
+
size?: number;
|
|
29
|
+
/** Additional container style */
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
/** Additional image style */
|
|
32
|
+
imageStyle?: StyleProp<ImageStyle>;
|
|
33
|
+
/** Custom renderer for non-image file placeholders */
|
|
34
|
+
renderPlaceholder?: (mimeType: string, fileName?: string) => React.ReactNode;
|
|
35
|
+
/** Custom file icon function (returns string emoji or React node) */
|
|
36
|
+
getFileIcon?: (mimeType: string) => React.ReactNode | string;
|
|
37
|
+
/** Custom labels for upload status overlay */
|
|
38
|
+
statusLabels?: UploadStatusLabels;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Preview component for a single attachment with upload status.
|
|
43
|
+
*
|
|
44
|
+
* Shows image thumbnail for images, file icon for other types.
|
|
45
|
+
* Includes upload status overlay with progress/retry/remove actions.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* // Basic usage
|
|
50
|
+
* <AttachmentPreview
|
|
51
|
+
* uri={attachment.localUri}
|
|
52
|
+
* mimeType={attachment.mimeType}
|
|
53
|
+
* phase={attachment.phase}
|
|
54
|
+
* progress={attachment.progress}
|
|
55
|
+
* onRetry={() => retryAttachment(attachment.attachmentId)}
|
|
56
|
+
* onRemove={() => removeAttachment(attachment.attachmentId)}
|
|
57
|
+
* />
|
|
58
|
+
*
|
|
59
|
+
* // With custom file icon
|
|
60
|
+
* <AttachmentPreview
|
|
61
|
+
* uri={attachment.localUri}
|
|
62
|
+
* mimeType={attachment.mimeType}
|
|
63
|
+
* phase={attachment.phase}
|
|
64
|
+
* progress={attachment.progress}
|
|
65
|
+
* getFileIcon={(mime) => {
|
|
66
|
+
* if (mime === 'application/pdf') return <PdfIcon />;
|
|
67
|
+
* return '📄';
|
|
68
|
+
* }}
|
|
69
|
+
* />
|
|
70
|
+
*
|
|
71
|
+
* // With custom placeholder
|
|
72
|
+
* <AttachmentPreview
|
|
73
|
+
* uri={attachment.localUri}
|
|
74
|
+
* mimeType={attachment.mimeType}
|
|
75
|
+
* phase={attachment.phase}
|
|
76
|
+
* progress={attachment.progress}
|
|
77
|
+
* renderPlaceholder={(mime, name) => (
|
|
78
|
+
* <MyCustomPlaceholder mimeType={mime} fileName={name} />
|
|
79
|
+
* )}
|
|
80
|
+
* />
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function AttachmentPreview({
|
|
84
|
+
uri,
|
|
85
|
+
mimeType,
|
|
86
|
+
fileName,
|
|
87
|
+
phase,
|
|
88
|
+
progress,
|
|
89
|
+
error,
|
|
90
|
+
onRetry,
|
|
91
|
+
onRemove,
|
|
92
|
+
size = 80,
|
|
93
|
+
style,
|
|
94
|
+
imageStyle,
|
|
95
|
+
renderPlaceholder,
|
|
96
|
+
getFileIcon: customGetFileIcon,
|
|
97
|
+
statusLabels,
|
|
98
|
+
}: AttachmentPreviewProps): React.JSX.Element {
|
|
99
|
+
const theme = useHarkenTheme();
|
|
100
|
+
const isImage = mimeType?.startsWith('image/') ?? true;
|
|
101
|
+
|
|
102
|
+
const renderFileContent = () => {
|
|
103
|
+
if (renderPlaceholder && mimeType) {
|
|
104
|
+
return renderPlaceholder(mimeType, fileName);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const icon = customGetFileIcon
|
|
108
|
+
? customGetFileIcon(mimeType ?? '')
|
|
109
|
+
: getDefaultFileIcon(mimeType);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View style={styles.filePreview}>
|
|
113
|
+
{typeof icon === 'string' ? (
|
|
114
|
+
<ThemedText style={styles.fileIcon}>{icon}</ThemedText>
|
|
115
|
+
) : (
|
|
116
|
+
icon
|
|
117
|
+
)}
|
|
118
|
+
{fileName && (
|
|
119
|
+
<ThemedText
|
|
120
|
+
variant="caption"
|
|
121
|
+
secondary
|
|
122
|
+
numberOfLines={2}
|
|
123
|
+
style={styles.fileName}
|
|
124
|
+
>
|
|
125
|
+
{fileName}
|
|
126
|
+
</ThemedText>
|
|
127
|
+
)}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<View
|
|
134
|
+
style={[
|
|
135
|
+
styles.container,
|
|
136
|
+
{
|
|
137
|
+
width: size,
|
|
138
|
+
height: size,
|
|
139
|
+
borderRadius: theme.radii.md,
|
|
140
|
+
backgroundColor: theme.colors.backgroundSecondary,
|
|
141
|
+
borderWidth: 1,
|
|
142
|
+
borderColor: theme.colors.border,
|
|
143
|
+
overflow: 'hidden',
|
|
144
|
+
},
|
|
145
|
+
style,
|
|
146
|
+
]}
|
|
147
|
+
>
|
|
148
|
+
{isImage ? (
|
|
149
|
+
<Image
|
|
150
|
+
source={{ uri }}
|
|
151
|
+
style={[styles.image, { width: size, height: size }, imageStyle]}
|
|
152
|
+
resizeMode="cover"
|
|
153
|
+
/>
|
|
154
|
+
) : (
|
|
155
|
+
renderFileContent()
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
<UploadStatusOverlay
|
|
159
|
+
phase={phase}
|
|
160
|
+
progress={progress}
|
|
161
|
+
error={error}
|
|
162
|
+
onRetry={onRetry}
|
|
163
|
+
onRemove={onRemove}
|
|
164
|
+
labels={statusLabels}
|
|
165
|
+
/>
|
|
166
|
+
</View>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get default file icon emoji based on MIME type.
|
|
172
|
+
*/
|
|
173
|
+
function getDefaultFileIcon(mimeType?: string): string {
|
|
174
|
+
if (!mimeType) return '📄';
|
|
175
|
+
|
|
176
|
+
if (mimeType.startsWith('image/')) return '🖼️';
|
|
177
|
+
if (mimeType.startsWith('video/')) return '🎬';
|
|
178
|
+
if (mimeType === 'application/pdf') return '📕';
|
|
179
|
+
if (mimeType.includes('spreadsheet') || mimeType.includes('excel'))
|
|
180
|
+
return '📊';
|
|
181
|
+
if (mimeType.includes('document') || mimeType.includes('word')) return '📝';
|
|
182
|
+
if (mimeType.includes('presentation') || mimeType.includes('powerpoint'))
|
|
183
|
+
return '📽️';
|
|
184
|
+
if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦';
|
|
185
|
+
|
|
186
|
+
return '📄';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const styles = StyleSheet.create({
|
|
190
|
+
container: {
|
|
191
|
+
position: 'relative',
|
|
192
|
+
},
|
|
193
|
+
image: {
|
|
194
|
+
backgroundColor: '#f0f0f0',
|
|
195
|
+
},
|
|
196
|
+
filePreview: {
|
|
197
|
+
flex: 1,
|
|
198
|
+
alignItems: 'center',
|
|
199
|
+
justifyContent: 'center',
|
|
200
|
+
padding: 8,
|
|
201
|
+
},
|
|
202
|
+
fileIcon: {
|
|
203
|
+
fontSize: 28,
|
|
204
|
+
},
|
|
205
|
+
fileName: {
|
|
206
|
+
marginTop: 4,
|
|
207
|
+
textAlign: 'center',
|
|
208
|
+
fontSize: 10,
|
|
209
|
+
},
|
|
210
|
+
});
|