@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.
Files changed (71) hide show
  1. package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +47 -1
  2. package/ios/ComnyxMediaPicker.m +6 -0
  3. package/ios/ComnyxMediaPicker.swift +59 -0
  4. package/lib/commonjs/NativeComnyxMediaPicker.js +27 -6
  5. package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -1
  6. package/lib/commonjs/assets/gallery.png +0 -0
  7. package/lib/commonjs/assets/video-play.png +0 -0
  8. package/lib/commonjs/components/ChatList.js +39 -10
  9. package/lib/commonjs/components/ChatList.js.map +1 -1
  10. package/lib/commonjs/components/CustomerForm.js +42 -3
  11. package/lib/commonjs/components/CustomerForm.js.map +1 -1
  12. package/lib/commonjs/components/MediaPickerButton.js +211 -14
  13. package/lib/commonjs/components/MediaPickerButton.js.map +1 -1
  14. package/lib/commonjs/components/MediaViewerModal.js +7 -0
  15. package/lib/commonjs/components/MediaViewerModal.js.map +1 -1
  16. package/lib/commonjs/components/MessageItem.js +0 -1
  17. package/lib/commonjs/components/MessageItem.js.map +1 -1
  18. package/lib/commonjs/constants/translations.js +87 -116
  19. package/lib/commonjs/constants/translations.js.map +1 -1
  20. package/lib/commonjs/version.js +1 -1
  21. package/lib/module/NativeComnyxMediaPicker.js +25 -6
  22. package/lib/module/NativeComnyxMediaPicker.js.map +1 -1
  23. package/lib/module/assets/gallery.png +0 -0
  24. package/lib/module/assets/video-play.png +0 -0
  25. package/lib/module/components/ChatList.js +40 -11
  26. package/lib/module/components/ChatList.js.map +1 -1
  27. package/lib/module/components/CustomerForm.js +44 -5
  28. package/lib/module/components/CustomerForm.js.map +1 -1
  29. package/lib/module/components/MediaPickerButton.js +215 -18
  30. package/lib/module/components/MediaPickerButton.js.map +1 -1
  31. package/lib/module/components/MediaViewerModal.js +8 -1
  32. package/lib/module/components/MediaViewerModal.js.map +1 -1
  33. package/lib/module/components/MessageItem.js +0 -1
  34. package/lib/module/components/MessageItem.js.map +1 -1
  35. package/lib/module/constants/translations.js +87 -116
  36. package/lib/module/constants/translations.js.map +1 -1
  37. package/lib/module/version.js +1 -1
  38. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +2 -0
  39. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -1
  40. package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
  41. package/lib/typescript/src/components/CustomerForm.d.ts.map +1 -1
  42. package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -1
  43. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -1
  44. package/lib/typescript/src/constants/translations.d.ts.map +1 -1
  45. package/lib/typescript/src/types/LocalizationKeys.d.ts +0 -1
  46. package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
  47. package/lib/typescript/src/version.d.ts +1 -1
  48. package/package.json +1 -1
  49. package/src/NativeComnyxMediaPicker.ts +28 -7
  50. package/src/assets/gallery.png +0 -0
  51. package/src/assets/video-play.png +0 -0
  52. package/src/components/ChatList.tsx +63 -15
  53. package/src/components/CustomerForm.tsx +64 -6
  54. package/src/components/MediaPickerButton.tsx +238 -17
  55. package/src/components/MediaViewerModal.tsx +8 -1
  56. package/src/components/MessageItem.tsx +0 -1
  57. package/src/constants/translations.ts +87 -116
  58. package/src/types/LocalizationKeys.ts +0 -1
  59. package/src/version.ts +1 -1
  60. package/android/generated/RCTAppDependencyProvider.h +0 -25
  61. package/android/generated/RCTAppDependencyProvider.mm +0 -55
  62. package/android/generated/RCTModulesConformingToProtocolsProvider.h +0 -18
  63. package/android/generated/RCTModulesConformingToProtocolsProvider.mm +0 -33
  64. package/android/generated/RCTThirdPartyComponentsProvider.h +0 -16
  65. package/android/generated/RCTThirdPartyComponentsProvider.mm +0 -23
  66. package/android/generated/ReactAppDependencyProvider.podspec +0 -34
  67. package/android/generated/jni/CMakeLists.txt +0 -36
  68. package/android/generated/jni/RNComnyxSpec-generated.cpp +0 -22
  69. package/android/generated/jni/RNComnyxSpec.h +0 -24
  70. package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI-generated.cpp +0 -17
  71. 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,CAyuD1D,CAAC"}
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"}
@@ -51,6 +51,5 @@ export type LocalizationKeys = {
51
51
  'chat.media.pick.title': string;
52
52
  'chat.media.pick.photo': string;
53
53
  'chat.media.pick.video': string;
54
- 'chat.media.pick.cancel': string;
55
54
  };
56
55
  //# sourceMappingURL=LocalizationKeys.d.ts.map
@@ -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;IAChC,wBAAwB,EAAE,MAAM,CAAC;CAClC,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.14.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.14.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
- export async function pickMedia(): Promise<MediaAsset[]> {
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 keyboardDidShowListener = Keyboard.addListener(
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 keyboardDidHideListener = Keyboard.addListener(
572
- 'keyboardDidHide',
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
- keyboardDidShowListener.remove();
581
- keyboardDidHideListener.remove();
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
- <KeyboardAvoidingView
624
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
625
- style={{ flex: 1 }}
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
- </KeyboardAvoidingView>
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
- <KeyboardAvoidingView
133
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
181
+ <View
134
182
  style={[
135
- { flex: 1, backgroundColor: themeColors.dark_background },
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
- </KeyboardAvoidingView>
420
+ </View>
363
421
  </>
364
422
  );
365
423
  }
@@ -1,12 +1,29 @@
1
- import { TouchableOpacity, Image } from 'react-native';
2
- import { useCallback } from 'react';
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 { pickMedia } from '../NativeComnyxMediaPicker';
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 showPicker = useCallback(async () => {
19
- const assets = await pickMedia();
20
- if (assets.length > 0) onMediaSelected(assets);
21
- }, [onMediaSelected]);
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
- <TouchableOpacity
25
- style={styles.button}
26
- onPress={showPicker}
27
- activeOpacity={activeOpacity}
28
- >
29
- <Image
30
- source={paperclipIcon}
31
- style={[styles.icon, { tintColor: themeColors.text }]}
32
- />
33
- </TouchableOpacity>
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
  ) : (
@@ -185,7 +185,6 @@ function TextMessageItem({
185
185
  /https?:\/\/(?:[-\w.])+(?::[0-9]+)?(?:\/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?/g,
186
186
  color: '#0066CC',
187
187
  navigate: (url) => {
188
- console.log('url', url);
189
188
  if (url) {
190
189
  Linking.openURL(url);
191
190
  }