@developer_tribe/react-native-comnyx 0.14.0 → 0.15.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/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +47 -1
- package/ios/ComnyxMediaPicker.m +6 -0
- package/ios/ComnyxMediaPicker.swift +59 -0
- package/lib/commonjs/NativeComnyxMediaPicker.js +27 -6
- package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -1
- package/lib/commonjs/assets/gallery.png +0 -0
- package/lib/commonjs/assets/video-play.png +0 -0
- package/lib/commonjs/components/ChatList.js +39 -10
- package/lib/commonjs/components/ChatList.js.map +1 -1
- package/lib/commonjs/components/CustomerForm.js +42 -3
- package/lib/commonjs/components/CustomerForm.js.map +1 -1
- package/lib/commonjs/components/MediaPickerButton.js +211 -14
- package/lib/commonjs/components/MediaPickerButton.js.map +1 -1
- package/lib/commonjs/components/MediaViewerModal.js +7 -0
- package/lib/commonjs/components/MediaViewerModal.js.map +1 -1
- package/lib/commonjs/components/MessageItem.js +0 -1
- package/lib/commonjs/components/MessageItem.js.map +1 -1
- package/lib/commonjs/constants/translations.js +87 -116
- package/lib/commonjs/constants/translations.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/NativeComnyxMediaPicker.js +25 -6
- package/lib/module/NativeComnyxMediaPicker.js.map +1 -1
- package/lib/module/assets/gallery.png +0 -0
- package/lib/module/assets/video-play.png +0 -0
- package/lib/module/components/ChatList.js +40 -11
- package/lib/module/components/ChatList.js.map +1 -1
- package/lib/module/components/CustomerForm.js +44 -5
- package/lib/module/components/CustomerForm.js.map +1 -1
- package/lib/module/components/MediaPickerButton.js +215 -18
- package/lib/module/components/MediaPickerButton.js.map +1 -1
- package/lib/module/components/MediaViewerModal.js +8 -1
- package/lib/module/components/MediaViewerModal.js.map +1 -1
- package/lib/module/components/MessageItem.js +0 -1
- package/lib/module/components/MessageItem.js.map +1 -1
- package/lib/module/constants/translations.js +87 -116
- package/lib/module/constants/translations.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +2 -0
- package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -1
- package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
- package/lib/typescript/src/components/CustomerForm.d.ts.map +1 -1
- package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -1
- package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -1
- package/lib/typescript/src/constants/translations.d.ts.map +1 -1
- package/lib/typescript/src/types/LocalizationKeys.d.ts +0 -1
- package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/NativeComnyxMediaPicker.ts +28 -7
- package/src/assets/gallery.png +0 -0
- package/src/assets/video-play.png +0 -0
- package/src/components/ChatList.tsx +63 -15
- package/src/components/CustomerForm.tsx +64 -6
- package/src/components/MediaPickerButton.tsx +238 -17
- package/src/components/MediaViewerModal.tsx +8 -1
- package/src/components/MessageItem.tsx +0 -1
- package/src/constants/translations.ts +87 -116
- package/src/types/LocalizationKeys.ts +0 -1
- package/src/version.ts +1 -1
- package/android/generated/RCTAppDependencyProvider.h +0 -25
- package/android/generated/RCTAppDependencyProvider.mm +0 -55
- package/android/generated/RCTModulesConformingToProtocolsProvider.h +0 -18
- package/android/generated/RCTModulesConformingToProtocolsProvider.mm +0 -33
- package/android/generated/RCTThirdPartyComponentsProvider.h +0 -16
- package/android/generated/RCTThirdPartyComponentsProvider.mm +0 -23
- package/android/generated/ReactAppDependencyProvider.podspec +0 -34
- package/android/generated/jni/CMakeLists.txt +0 -36
- package/android/generated/jni/RNComnyxSpec-generated.cpp +0 -22
- package/android/generated/jni/RNComnyxSpec.h +0 -24
- package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI-generated.cpp +0 -17
- package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI.h +0 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../../src/constants/translations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,YAAY,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../../src/constants/translations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,YAAY,EAAE,gBAAgB,CA4sD1D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalizationKeys.d.ts","sourceRoot":"","sources":["../../../../src/types/LocalizationKeys.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,uBAAuB,EAAE,MAAM,CAAC;IAChC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,4BAA4B,EAAE,MAAM,CAAC;IACrC,6BAA6B,EAAE,MAAM,CAAC;IACtC,2BAA2B,EAAE,MAAM,CAAC;IACpC,2BAA2B,EAAE,MAAM,CAAC;IACpC,gCAAgC,EAAE,MAAM,CAAC;IACzC,mCAAmC,EAAE,MAAM,CAAC;IAC5C,iCAAiC,EAAE,MAAM,CAAC;IAC1C,iCAAiC,EAAE,MAAM,CAAC;IAC1C,wCAAwC,EAAE,MAAM,CAAC;IACjD,wCAAwC,EAAE,MAAM,CAAC;IACjD,4CAA4C,EAAE,MAAM,CAAC;IACrD,yCAAyC,EAAE,MAAM,CAAC;IAClD,wCAAwC,EAAE,MAAM,CAAC;IACjD,+BAA+B,EAAE,MAAM,CAAC;IACxC,6BAA6B,EAAE,MAAM,CAAC;IACtC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wBAAwB,EAAE,MAAM,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gCAAgC,EAAE,MAAM,CAAC;IACzC,sCAAsC,EAAE,MAAM,CAAC;IAC/C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2BAA2B,EAAE,MAAM,CAAC;IACpC,gCAAgC,EAAE,MAAM,CAAC;IACzC,4BAA4B,EAAE,MAAM,CAAC;IACrC,iCAAiC,EAAE,MAAM,CAAC;IAC1C,4BAA4B,EAAE,MAAM,CAAC;IACrC,iCAAiC,EAAE,MAAM,CAAC;IAC1C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,2BAA2B,EAAE,MAAM,CAAC;IACpC,iCAAiC,EAAE,MAAM,CAAC;IAC1C,4BAA4B,EAAE,MAAM,CAAC;IACrC,kCAAkC,EAAE,MAAM,CAAC;IAC3C,qCAAqC,EAAE,MAAM,CAAC;IAC9C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,0BAA0B,EAAE,MAAM,CAAC;IACnC,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"LocalizationKeys.d.ts","sourceRoot":"","sources":["../../../../src/types/LocalizationKeys.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,uBAAuB,EAAE,MAAM,CAAC;IAChC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,4BAA4B,EAAE,MAAM,CAAC;IACrC,6BAA6B,EAAE,MAAM,CAAC;IACtC,2BAA2B,EAAE,MAAM,CAAC;IACpC,2BAA2B,EAAE,MAAM,CAAC;IACpC,gCAAgC,EAAE,MAAM,CAAC;IACzC,mCAAmC,EAAE,MAAM,CAAC;IAC5C,iCAAiC,EAAE,MAAM,CAAC;IAC1C,iCAAiC,EAAE,MAAM,CAAC;IAC1C,wCAAwC,EAAE,MAAM,CAAC;IACjD,wCAAwC,EAAE,MAAM,CAAC;IACjD,4CAA4C,EAAE,MAAM,CAAC;IACrD,yCAAyC,EAAE,MAAM,CAAC;IAClD,wCAAwC,EAAE,MAAM,CAAC;IACjD,+BAA+B,EAAE,MAAM,CAAC;IACxC,6BAA6B,EAAE,MAAM,CAAC;IACtC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wBAAwB,EAAE,MAAM,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gCAAgC,EAAE,MAAM,CAAC;IACzC,sCAAsC,EAAE,MAAM,CAAC;IAC/C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2BAA2B,EAAE,MAAM,CAAC;IACpC,gCAAgC,EAAE,MAAM,CAAC;IACzC,4BAA4B,EAAE,MAAM,CAAC;IACrC,iCAAiC,EAAE,MAAM,CAAC;IAC1C,4BAA4B,EAAE,MAAM,CAAC;IACrC,iCAAiC,EAAE,MAAM,CAAC;IAC1C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,2BAA2B,EAAE,MAAM,CAAC;IACpC,iCAAiC,EAAE,MAAM,CAAC;IAC1C,4BAA4B,EAAE,MAAM,CAAC;IACrC,kCAAkC,EAAE,MAAM,CAAC;IAC3C,qCAAqC,EAAE,MAAM,CAAC;IAC9C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,0BAA0B,EAAE,MAAM,CAAC;IACnC,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.15.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@developer_tribe/react-native-comnyx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "React Native chat component with integrated support panel, enabling real-time customer communication and efficient agent workflow management.",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -3,13 +3,7 @@ import type { MediaAsset } from './types/MediaTypes';
|
|
|
3
3
|
|
|
4
4
|
const { ComnyxMediaPicker } = NativeModules;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
if (!ComnyxMediaPicker) {
|
|
8
|
-
return [];
|
|
9
|
-
}
|
|
10
|
-
const results = await ComnyxMediaPicker.pickMedia();
|
|
11
|
-
if (!results || !Array.isArray(results) || results.length === 0) return [];
|
|
12
|
-
|
|
6
|
+
function mapResults(results: any[]): MediaAsset[] {
|
|
13
7
|
return results.map((result: any) => {
|
|
14
8
|
const isImage =
|
|
15
9
|
result.type === 'image' ||
|
|
@@ -29,6 +23,33 @@ export async function pickMedia(): Promise<MediaAsset[]> {
|
|
|
29
23
|
});
|
|
30
24
|
}
|
|
31
25
|
|
|
26
|
+
export async function pickMedia(): Promise<MediaAsset[]> {
|
|
27
|
+
if (!ComnyxMediaPicker) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const results = await ComnyxMediaPicker.pickMedia();
|
|
31
|
+
if (!results || !Array.isArray(results) || results.length === 0) return [];
|
|
32
|
+
return mapResults(results);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function pickImage(): Promise<MediaAsset[]> {
|
|
36
|
+
if (!ComnyxMediaPicker) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const results = await ComnyxMediaPicker.pickImage();
|
|
40
|
+
if (!results || !Array.isArray(results) || results.length === 0) return [];
|
|
41
|
+
return mapResults(results);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function pickVideo(): Promise<MediaAsset[]> {
|
|
45
|
+
if (!ComnyxMediaPicker) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const results = await ComnyxMediaPicker.pickVideo();
|
|
49
|
+
if (!results || !Array.isArray(results) || results.length === 0) return [];
|
|
50
|
+
return mapResults(results);
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
export async function openVideo(uri: string): Promise<void> {
|
|
33
54
|
if (!ComnyxMediaPicker) {
|
|
34
55
|
console.warn('[Comnyx] ComnyxMediaPicker native module is not available');
|
|
Binary file
|
|
Binary file
|
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
TouchableOpacity,
|
|
9
9
|
Keyboard,
|
|
10
10
|
StatusBar,
|
|
11
|
-
KeyboardAvoidingView,
|
|
12
11
|
Platform,
|
|
13
12
|
Animated,
|
|
13
|
+
LayoutAnimation,
|
|
14
|
+
UIManager,
|
|
14
15
|
type SectionListData,
|
|
15
16
|
} from 'react-native';
|
|
16
17
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
@@ -32,6 +33,13 @@ import { formatDate, getDateKey } from '../utils/formatDate';
|
|
|
32
33
|
import { activeOpacity } from '../constants/activeOpacity';
|
|
33
34
|
import { useAppStore } from '../store/store';
|
|
34
35
|
|
|
36
|
+
if (
|
|
37
|
+
Platform.OS === 'android' &&
|
|
38
|
+
UIManager.setLayoutAnimationEnabledExperimental
|
|
39
|
+
) {
|
|
40
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
const headphonesIcon = require('../assets/headphones-01.png');
|
|
36
44
|
const closeIcon = require('../assets/x-close.png');
|
|
37
45
|
|
|
@@ -197,6 +205,7 @@ export function ChatList({
|
|
|
197
205
|
const [initFailed, setInitFailed] = useState(false);
|
|
198
206
|
const [showScrollDownButton, setShowScrollDownButton] = useState(false);
|
|
199
207
|
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
|
208
|
+
const [keyboardPadding, setKeyboardPadding] = useState(0);
|
|
200
209
|
const listChangedRef = useRef(false);
|
|
201
210
|
const [popupVisible, setPopupVisible] = useState(false);
|
|
202
211
|
const [selectedMessage, setSelectedMessage] = useState<string>('');
|
|
@@ -560,25 +569,55 @@ export function ChatList({
|
|
|
560
569
|
}
|
|
561
570
|
}, [MESSAGES_PER_PAGE, customer?.external_id, initFailed, setData]);
|
|
562
571
|
|
|
563
|
-
// Add keyboard listeners
|
|
572
|
+
// Add keyboard listeners.
|
|
573
|
+
// We deliberately bypass RN's KeyboardAvoidingView on Android — under RN 0.85
|
|
574
|
+
// edge-to-edge (targetSdk 36) its `_relativeKeyboardHeight()` math mismatches
|
|
575
|
+
// the keyboard frame against the navigation-bar inset and leaves a residual
|
|
576
|
+
// gap both when the keyboard opens and when it closes. Reading
|
|
577
|
+
// `endCoordinates.height` directly and applying it as paddingBottom on the
|
|
578
|
+
// outer container produces the correct offset on every device.
|
|
564
579
|
useEffect(() => {
|
|
565
|
-
const
|
|
566
|
-
'keyboardDidShow'
|
|
567
|
-
|
|
580
|
+
const showEventName =
|
|
581
|
+
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
582
|
+
const hideEventName =
|
|
583
|
+
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
584
|
+
|
|
585
|
+
const animateNext = (duration?: number, easing?: string) => {
|
|
586
|
+
const d = duration && duration > 10 ? duration : 250;
|
|
587
|
+
const types = LayoutAnimation.Types as Record<
|
|
588
|
+
string,
|
|
589
|
+
(typeof LayoutAnimation.Types)[keyof typeof LayoutAnimation.Types]
|
|
590
|
+
>;
|
|
591
|
+
LayoutAnimation.configureNext({
|
|
592
|
+
duration: d,
|
|
593
|
+
update: {
|
|
594
|
+
duration: d,
|
|
595
|
+
type: (easing && types[easing]) || LayoutAnimation.Types.keyboard,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const keyboardShowListener = Keyboard.addListener(
|
|
601
|
+
showEventName,
|
|
602
|
+
(event: any) => {
|
|
603
|
+
const height = event?.endCoordinates?.height ?? 0;
|
|
604
|
+
animateNext(event?.duration, event?.easing);
|
|
605
|
+
setKeyboardPadding(height);
|
|
568
606
|
setIsKeyboardVisible(true);
|
|
569
607
|
}
|
|
570
608
|
);
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
() => {
|
|
609
|
+
const keyboardHideListener = Keyboard.addListener(
|
|
610
|
+
hideEventName,
|
|
611
|
+
(event: any) => {
|
|
612
|
+
animateNext(event?.duration, event?.easing);
|
|
613
|
+
setKeyboardPadding(0);
|
|
574
614
|
setIsKeyboardVisible(false);
|
|
575
615
|
}
|
|
576
616
|
);
|
|
577
617
|
|
|
578
|
-
// Cleanup function
|
|
579
618
|
return () => {
|
|
580
|
-
|
|
581
|
-
|
|
619
|
+
keyboardShowListener.remove();
|
|
620
|
+
keyboardHideListener.remove();
|
|
582
621
|
};
|
|
583
622
|
}, []);
|
|
584
623
|
|
|
@@ -620,9 +659,18 @@ export function ChatList({
|
|
|
620
659
|
animated={false}
|
|
621
660
|
translucent
|
|
622
661
|
/>
|
|
623
|
-
<
|
|
624
|
-
|
|
625
|
-
|
|
662
|
+
<View
|
|
663
|
+
style={{
|
|
664
|
+
flex: 1,
|
|
665
|
+
// Android edge-to-edge: keyboardDidShow.endCoordinates.height excludes
|
|
666
|
+
// the navigation-bar inset, so the keyboard's true visual height is
|
|
667
|
+
// `kbHeight + insets.bottom`. iOS reports the full height already.
|
|
668
|
+
paddingBottom:
|
|
669
|
+
keyboardPadding > 0
|
|
670
|
+
? keyboardPadding +
|
|
671
|
+
(Platform.OS === 'android' ? insets.bottom : 0)
|
|
672
|
+
: insets.bottom,
|
|
673
|
+
}}
|
|
626
674
|
>
|
|
627
675
|
<View
|
|
628
676
|
style={[
|
|
@@ -749,7 +797,7 @@ export function ChatList({
|
|
|
749
797
|
/>
|
|
750
798
|
</View>
|
|
751
799
|
<CustomToast />
|
|
752
|
-
</
|
|
800
|
+
</View>
|
|
753
801
|
</>
|
|
754
802
|
);
|
|
755
803
|
}
|
|
@@ -7,23 +7,32 @@ import {
|
|
|
7
7
|
ScrollView,
|
|
8
8
|
StatusBar,
|
|
9
9
|
ActivityIndicator,
|
|
10
|
-
KeyboardAvoidingView,
|
|
11
10
|
Platform,
|
|
11
|
+
LayoutAnimation,
|
|
12
|
+
UIManager,
|
|
12
13
|
type ViewStyle,
|
|
13
14
|
type StyleProp,
|
|
14
15
|
} from 'react-native';
|
|
15
16
|
import { useForm, Controller } from 'react-hook-form';
|
|
17
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
16
18
|
import { createCustomer } from '../api';
|
|
17
19
|
import { AppText } from './AppText';
|
|
18
20
|
import { useLocalize } from '../hooks/useLocalize';
|
|
19
21
|
import { useThemeColors } from '../hooks/useThemeColors';
|
|
20
22
|
import CustomPopup from './CustomAlert';
|
|
21
|
-
import { useState } from 'react';
|
|
23
|
+
import { useState, useEffect } from 'react';
|
|
22
24
|
import { ScaledSheet } from './ScaledSheet';
|
|
23
25
|
import type { LocalizationKeys } from '../types/LocalizationKeys';
|
|
24
26
|
import { activeOpacity } from '../constants/activeOpacity';
|
|
25
27
|
import { useAppStore } from '../store/store';
|
|
26
28
|
|
|
29
|
+
if (
|
|
30
|
+
Platform.OS === 'android' &&
|
|
31
|
+
UIManager.setLayoutAnimationEnabledExperimental
|
|
32
|
+
) {
|
|
33
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
interface CustomerFormData {
|
|
28
37
|
name: string;
|
|
29
38
|
country: string;
|
|
@@ -57,6 +66,7 @@ export function CustomerForm({
|
|
|
57
66
|
|
|
58
67
|
const themeColors = useThemeColors();
|
|
59
68
|
const localize = useLocalize();
|
|
69
|
+
const insets = useSafeAreaInsets();
|
|
60
70
|
|
|
61
71
|
const inputStyle = {
|
|
62
72
|
...styles.input,
|
|
@@ -69,6 +79,45 @@ export function CustomerForm({
|
|
|
69
79
|
title: '',
|
|
70
80
|
description: 'null',
|
|
71
81
|
});
|
|
82
|
+
const [keyboardPadding, setKeyboardPadding] = useState(0);
|
|
83
|
+
|
|
84
|
+
// Manual keyboard handling — see ChatList for the same rationale: RN 0.85
|
|
85
|
+
// KeyboardAvoidingView miscomputes on Android edge-to-edge.
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const showEventName =
|
|
88
|
+
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
89
|
+
const hideEventName =
|
|
90
|
+
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
91
|
+
|
|
92
|
+
const animateNext = (duration?: number, easing?: string) => {
|
|
93
|
+
const d = duration && duration > 10 ? duration : 250;
|
|
94
|
+
const types = LayoutAnimation.Types as Record<
|
|
95
|
+
string,
|
|
96
|
+
(typeof LayoutAnimation.Types)[keyof typeof LayoutAnimation.Types]
|
|
97
|
+
>;
|
|
98
|
+
LayoutAnimation.configureNext({
|
|
99
|
+
duration: d,
|
|
100
|
+
update: {
|
|
101
|
+
duration: d,
|
|
102
|
+
type: (easing && types[easing]) || LayoutAnimation.Types.keyboard,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const showSub = Keyboard.addListener(showEventName, (event: any) => {
|
|
108
|
+
const height = event?.endCoordinates?.height ?? 0;
|
|
109
|
+
animateNext(event?.duration, event?.easing);
|
|
110
|
+
setKeyboardPadding(height);
|
|
111
|
+
});
|
|
112
|
+
const hideSub = Keyboard.addListener(hideEventName, (event: any) => {
|
|
113
|
+
animateNext(event?.duration, event?.easing);
|
|
114
|
+
setKeyboardPadding(0);
|
|
115
|
+
});
|
|
116
|
+
return () => {
|
|
117
|
+
showSub.remove();
|
|
118
|
+
hideSub.remove();
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
72
121
|
|
|
73
122
|
const onSubmit = async (data: CustomerFormData) => {
|
|
74
123
|
try {
|
|
@@ -129,10 +178,19 @@ export function CustomerForm({
|
|
|
129
178
|
animated={false}
|
|
130
179
|
translucent
|
|
131
180
|
/>
|
|
132
|
-
<
|
|
133
|
-
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
181
|
+
<View
|
|
134
182
|
style={[
|
|
135
|
-
{
|
|
183
|
+
{
|
|
184
|
+
flex: 1,
|
|
185
|
+
backgroundColor: themeColors.dark_background,
|
|
186
|
+
// Android edge-to-edge: keyboardDidShow height excludes the nav-bar
|
|
187
|
+
// inset; add it back so the input clears the soft keyboard.
|
|
188
|
+
paddingBottom:
|
|
189
|
+
keyboardPadding > 0
|
|
190
|
+
? keyboardPadding +
|
|
191
|
+
(Platform.OS === 'android' ? insets.bottom : 0)
|
|
192
|
+
: insets.bottom,
|
|
193
|
+
},
|
|
136
194
|
containerStyle,
|
|
137
195
|
]}
|
|
138
196
|
>
|
|
@@ -359,7 +417,7 @@ export function CustomerForm({
|
|
|
359
417
|
buttonText={'customer.form.cancel' as keyof LocalizationKeys}
|
|
360
418
|
/>
|
|
361
419
|
</TouchableOpacity>
|
|
362
|
-
</
|
|
420
|
+
</View>
|
|
363
421
|
</>
|
|
364
422
|
);
|
|
365
423
|
}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
TouchableOpacity,
|
|
3
|
+
Image,
|
|
4
|
+
View,
|
|
5
|
+
Modal,
|
|
6
|
+
Animated,
|
|
7
|
+
Dimensions,
|
|
8
|
+
TouchableWithoutFeedback,
|
|
9
|
+
Platform,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useState, useCallback, useRef } from 'react';
|
|
3
12
|
import { useThemeColors } from '../hooks/useThemeColors';
|
|
4
13
|
import { ScaledSheet } from './ScaledSheet';
|
|
5
14
|
import { activeOpacity } from '../constants/activeOpacity';
|
|
6
|
-
import {
|
|
15
|
+
import { pickImage, pickVideo } from '../NativeComnyxMediaPicker';
|
|
16
|
+
import { AppText } from './AppText';
|
|
17
|
+
import { useLocalize } from '../hooks/useLocalize';
|
|
7
18
|
import type { MediaAsset } from '../types/MediaTypes';
|
|
8
19
|
|
|
9
20
|
const paperclipIcon = require('../assets/attachment-01.png');
|
|
21
|
+
const galleryIcon = require('../assets/gallery.png');
|
|
22
|
+
const videoPlayIcon = require('../assets/video-play.png');
|
|
23
|
+
|
|
24
|
+
const closeCircleIcon = require('../assets/x-circle.png');
|
|
25
|
+
|
|
26
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
10
27
|
|
|
11
28
|
export function MediaPickerButton({
|
|
12
29
|
onMediaSelected,
|
|
@@ -14,23 +31,166 @@ export function MediaPickerButton({
|
|
|
14
31
|
onMediaSelected: (assets: MediaAsset[]) => void;
|
|
15
32
|
}) {
|
|
16
33
|
const themeColors = useThemeColors();
|
|
34
|
+
const localize = useLocalize();
|
|
35
|
+
const [visible, setVisible] = useState(false);
|
|
36
|
+
const slideAnim = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
|
|
37
|
+
|
|
38
|
+
const open = useCallback(() => {
|
|
39
|
+
setVisible(true);
|
|
40
|
+
Animated.spring(slideAnim, {
|
|
41
|
+
toValue: 0,
|
|
42
|
+
useNativeDriver: true,
|
|
43
|
+
bounciness: 4,
|
|
44
|
+
speed: 14,
|
|
45
|
+
}).start();
|
|
46
|
+
}, [slideAnim]);
|
|
47
|
+
|
|
48
|
+
const close = useCallback(() => {
|
|
49
|
+
Animated.timing(slideAnim, {
|
|
50
|
+
toValue: SCREEN_HEIGHT,
|
|
51
|
+
duration: 250,
|
|
52
|
+
useNativeDriver: true,
|
|
53
|
+
}).start(() => {
|
|
54
|
+
setVisible(false);
|
|
55
|
+
});
|
|
56
|
+
}, [slideAnim]);
|
|
57
|
+
|
|
58
|
+
const pendingAction = useRef<(() => void) | null>(null);
|
|
59
|
+
|
|
60
|
+
const dismissAndRun = useCallback(
|
|
61
|
+
(action: () => void) => {
|
|
62
|
+
slideAnim.setValue(SCREEN_HEIGHT);
|
|
63
|
+
if (Platform.OS === 'ios') {
|
|
64
|
+
// Defer action until Modal's onDismiss fires to avoid
|
|
65
|
+
// "Attempt to present on a VC which is already presenting"
|
|
66
|
+
pendingAction.current = action;
|
|
67
|
+
setVisible(false);
|
|
68
|
+
} else {
|
|
69
|
+
setVisible(false);
|
|
70
|
+
action();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[slideAnim]
|
|
74
|
+
);
|
|
17
75
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
76
|
+
const handlePickImage = useCallback(async () => {
|
|
77
|
+
dismissAndRun(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const assets = await pickImage();
|
|
80
|
+
if (assets.length > 0) onMediaSelected(assets);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.error('[Comnyx] pickImage error:', e);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}, [dismissAndRun, onMediaSelected]);
|
|
86
|
+
|
|
87
|
+
const handlePickVideo = useCallback(async () => {
|
|
88
|
+
dismissAndRun(async () => {
|
|
89
|
+
try {
|
|
90
|
+
const assets = await pickVideo();
|
|
91
|
+
if (assets.length > 0) onMediaSelected(assets);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error('[Comnyx] pickVideo error:', e);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}, [dismissAndRun, onMediaSelected]);
|
|
22
97
|
|
|
23
98
|
return (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
99
|
+
<>
|
|
100
|
+
<TouchableOpacity
|
|
101
|
+
style={styles.button}
|
|
102
|
+
onPress={open}
|
|
103
|
+
activeOpacity={activeOpacity}
|
|
104
|
+
>
|
|
105
|
+
<Image
|
|
106
|
+
source={paperclipIcon}
|
|
107
|
+
style={[styles.icon, { tintColor: themeColors.text }]}
|
|
108
|
+
/>
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
|
|
111
|
+
<Modal
|
|
112
|
+
visible={visible}
|
|
113
|
+
transparent
|
|
114
|
+
animationType="none"
|
|
115
|
+
onRequestClose={close}
|
|
116
|
+
onDismiss={() => {
|
|
117
|
+
if (pendingAction.current) {
|
|
118
|
+
const action = pendingAction.current;
|
|
119
|
+
pendingAction.current = null;
|
|
120
|
+
action();
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
statusBarTranslucent
|
|
124
|
+
>
|
|
125
|
+
<TouchableWithoutFeedback onPress={close}>
|
|
126
|
+
<View style={styles.overlay}>
|
|
127
|
+
<TouchableWithoutFeedback>
|
|
128
|
+
<Animated.View
|
|
129
|
+
style={[
|
|
130
|
+
styles.sheet,
|
|
131
|
+
{
|
|
132
|
+
backgroundColor: themeColors.background,
|
|
133
|
+
transform: [{ translateY: slideAnim }],
|
|
134
|
+
},
|
|
135
|
+
]}
|
|
136
|
+
>
|
|
137
|
+
<View style={styles.handle} />
|
|
138
|
+
|
|
139
|
+
<View style={styles.titleRow}>
|
|
140
|
+
<AppText style={[styles.title, { color: themeColors.text }]}>
|
|
141
|
+
{localize('chat.media.pick.title')}
|
|
142
|
+
</AppText>
|
|
143
|
+
{Platform.OS === 'android' && (
|
|
144
|
+
<TouchableOpacity
|
|
145
|
+
onPress={close}
|
|
146
|
+
activeOpacity={activeOpacity}
|
|
147
|
+
style={styles.closeButton}
|
|
148
|
+
>
|
|
149
|
+
<Image
|
|
150
|
+
source={closeCircleIcon}
|
|
151
|
+
style={styles.closeIcon}
|
|
152
|
+
/>
|
|
153
|
+
</TouchableOpacity>
|
|
154
|
+
)}
|
|
155
|
+
</View>
|
|
156
|
+
|
|
157
|
+
<TouchableOpacity
|
|
158
|
+
style={styles.option}
|
|
159
|
+
onPress={handlePickImage}
|
|
160
|
+
activeOpacity={activeOpacity}
|
|
161
|
+
>
|
|
162
|
+
<Image
|
|
163
|
+
source={galleryIcon}
|
|
164
|
+
style={[styles.optionIcon, { tintColor: themeColors.text }]}
|
|
165
|
+
/>
|
|
166
|
+
<AppText
|
|
167
|
+
style={[styles.optionText, { color: themeColors.text }]}
|
|
168
|
+
>
|
|
169
|
+
{localize('chat.media.pick.photo')}
|
|
170
|
+
</AppText>
|
|
171
|
+
</TouchableOpacity>
|
|
172
|
+
|
|
173
|
+
<TouchableOpacity
|
|
174
|
+
style={styles.option}
|
|
175
|
+
onPress={handlePickVideo}
|
|
176
|
+
activeOpacity={activeOpacity}
|
|
177
|
+
>
|
|
178
|
+
<Image
|
|
179
|
+
source={videoPlayIcon}
|
|
180
|
+
style={[styles.optionIcon, { tintColor: themeColors.text }]}
|
|
181
|
+
/>
|
|
182
|
+
<AppText
|
|
183
|
+
style={[styles.optionText, { color: themeColors.text }]}
|
|
184
|
+
>
|
|
185
|
+
{localize('chat.media.pick.video')}
|
|
186
|
+
</AppText>
|
|
187
|
+
</TouchableOpacity>
|
|
188
|
+
</Animated.View>
|
|
189
|
+
</TouchableWithoutFeedback>
|
|
190
|
+
</View>
|
|
191
|
+
</TouchableWithoutFeedback>
|
|
192
|
+
</Modal>
|
|
193
|
+
</>
|
|
34
194
|
);
|
|
35
195
|
}
|
|
36
196
|
|
|
@@ -45,4 +205,65 @@ const styles = ScaledSheet.create({
|
|
|
45
205
|
width: '28@vs',
|
|
46
206
|
height: '28@vs',
|
|
47
207
|
},
|
|
208
|
+
overlay: {
|
|
209
|
+
flex: 1,
|
|
210
|
+
backgroundColor: 'rgba(0,0,0,0.4)',
|
|
211
|
+
justifyContent: 'flex-end',
|
|
212
|
+
},
|
|
213
|
+
sheet: {
|
|
214
|
+
borderTopLeftRadius: '16@vs',
|
|
215
|
+
borderTopRightRadius: '16@vs',
|
|
216
|
+
paddingBottom: '34@vs',
|
|
217
|
+
paddingTop: '12@vs',
|
|
218
|
+
paddingHorizontal: '20@s',
|
|
219
|
+
},
|
|
220
|
+
handle: {
|
|
221
|
+
width: '36@s',
|
|
222
|
+
height: '4@vs',
|
|
223
|
+
backgroundColor: '#ccc',
|
|
224
|
+
borderRadius: '2@vs',
|
|
225
|
+
alignSelf: 'center',
|
|
226
|
+
marginBottom: '16@vs',
|
|
227
|
+
},
|
|
228
|
+
titleRow: {
|
|
229
|
+
flexDirection: 'row',
|
|
230
|
+
justifyContent: 'space-between',
|
|
231
|
+
alignItems: 'center',
|
|
232
|
+
marginBottom: '16@vs',
|
|
233
|
+
},
|
|
234
|
+
title: {
|
|
235
|
+
fontSize: '18@vs',
|
|
236
|
+
fontWeight: '600',
|
|
237
|
+
},
|
|
238
|
+
closeButton: {
|
|
239
|
+
padding: '4@vs',
|
|
240
|
+
},
|
|
241
|
+
closeIcon: {
|
|
242
|
+
width: '22@vs',
|
|
243
|
+
height: '22@vs',
|
|
244
|
+
},
|
|
245
|
+
optionIcon: {
|
|
246
|
+
width: '24@vs',
|
|
247
|
+
height: '24@vs',
|
|
248
|
+
marginRight: '12@s',
|
|
249
|
+
},
|
|
250
|
+
option: {
|
|
251
|
+
paddingVertical: '14@vs',
|
|
252
|
+
flexDirection: 'row',
|
|
253
|
+
alignItems: 'center',
|
|
254
|
+
},
|
|
255
|
+
optionText: {
|
|
256
|
+
fontSize: '16@vs',
|
|
257
|
+
},
|
|
258
|
+
cancelButton: {
|
|
259
|
+
marginTop: '8@vs',
|
|
260
|
+
paddingVertical: '14@vs',
|
|
261
|
+
alignItems: 'center',
|
|
262
|
+
borderTopWidth: 0.5,
|
|
263
|
+
borderTopColor: '#ddd',
|
|
264
|
+
},
|
|
265
|
+
cancelText: {
|
|
266
|
+
fontSize: '16@vs',
|
|
267
|
+
opacity: 0.6,
|
|
268
|
+
},
|
|
48
269
|
});
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
StatusBar,
|
|
7
7
|
useWindowDimensions,
|
|
8
8
|
} from 'react-native';
|
|
9
|
-
import { useState } from 'react';
|
|
9
|
+
import { useState, useEffect } from 'react';
|
|
10
10
|
import { AppText } from './AppText';
|
|
11
11
|
import { ScaledSheet } from './ScaledSheet';
|
|
12
12
|
import { activeOpacity } from '../constants/activeOpacity';
|
|
@@ -29,6 +29,12 @@ export function MediaViewerModal({
|
|
|
29
29
|
const [imageError, setImageError] = useState(false);
|
|
30
30
|
const isVideo = mediaType === 'video';
|
|
31
31
|
const displayUri = isVideo ? thumbnailUri || undefined : mediaUri;
|
|
32
|
+
// Reset error state when URI or visibility changes
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (visible) {
|
|
35
|
+
setImageError(false);
|
|
36
|
+
}
|
|
37
|
+
}, [visible, mediaUri]);
|
|
32
38
|
|
|
33
39
|
const handlePlayVideo = () => {
|
|
34
40
|
if (mediaUri) {
|
|
@@ -61,6 +67,7 @@ export function MediaViewerModal({
|
|
|
61
67
|
source={{ uri: displayUri }}
|
|
62
68
|
style={{ width, height: height * 0.9 }}
|
|
63
69
|
resizeMode="contain"
|
|
70
|
+
resizeMethod="resize"
|
|
64
71
|
onError={() => setImageError(true)}
|
|
65
72
|
/>
|
|
66
73
|
) : (
|