@droppii-org/chat-mobile 0.2.107 → 0.2.108

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 (123) hide show
  1. package/lib/module/components/ThreadCard/ThreadCard.js +2 -0
  2. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  3. package/lib/module/components/messages/fileMessage/index.js +9 -2
  4. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  5. package/lib/module/components/messages/quotedMessage/index.js +6 -1
  6. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  7. package/lib/module/context/TimelinePressGuardContext.js +14 -0
  8. package/lib/module/context/TimelinePressGuardContext.js.map +1 -0
  9. package/lib/module/hooks/useConversationList.js +4 -0
  10. package/lib/module/hooks/useConversationList.js.map +1 -1
  11. package/lib/module/hooks/useDismissKeyboardFirst.js +19 -0
  12. package/lib/module/hooks/useDismissKeyboardFirst.js.map +1 -0
  13. package/lib/module/hooks/useExpandableText.js +6 -1
  14. package/lib/module/hooks/useExpandableText.js.map +1 -1
  15. package/lib/module/hooks/useGuardedPress.js +19 -0
  16. package/lib/module/hooks/useGuardedPress.js.map +1 -0
  17. package/lib/module/screens/chat-detail/ChatComposer.js +89 -498
  18. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  19. package/lib/module/screens/chat-detail/ChatDetail.js +38 -20
  20. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  21. package/lib/module/screens/chat-detail/ChatListLegend.js +113 -76
  22. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  23. package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js +47 -0
  24. package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js.map +1 -0
  25. package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js +149 -0
  26. package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js.map +1 -0
  27. package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js +104 -0
  28. package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js.map +1 -0
  29. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +0 -17
  30. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
  31. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +16 -2
  32. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
  33. package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js +122 -0
  34. package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js.map +1 -0
  35. package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js +46 -0
  36. package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js.map +1 -0
  37. package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js +75 -0
  38. package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js.map +1 -0
  39. package/lib/module/screens/chat-detail/hooks/useComposerPanels.js +136 -0
  40. package/lib/module/screens/chat-detail/hooks/useComposerPanels.js.map +1 -0
  41. package/lib/module/screens/chat-detail/hooks/useComposerSend.js +74 -0
  42. package/lib/module/screens/chat-detail/hooks/useComposerSend.js.map +1 -0
  43. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +2 -0
  44. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  45. package/lib/module/screens/chat-detail/types.js +5 -0
  46. package/lib/module/screens/chat-detail/types.js.map +1 -1
  47. package/lib/module/store/message.js +14 -0
  48. package/lib/module/store/message.js.map +1 -1
  49. package/lib/module/utils/composerPanelDismissGesture.js +52 -0
  50. package/lib/module/utils/composerPanelDismissGesture.js.map +1 -0
  51. package/lib/module/utils/perf-marker.js +44 -0
  52. package/lib/module/utils/perf-marker.js.map +1 -0
  53. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  54. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  55. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  56. package/lib/typescript/src/context/TimelinePressGuardContext.d.ts +3 -0
  57. package/lib/typescript/src/context/TimelinePressGuardContext.d.ts.map +1 -0
  58. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  59. package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts +8 -0
  60. package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts.map +1 -0
  61. package/lib/typescript/src/hooks/useExpandableText.d.ts.map +1 -1
  62. package/lib/typescript/src/hooks/useGuardedPress.d.ts +9 -0
  63. package/lib/typescript/src/hooks/useGuardedPress.d.ts.map +1 -0
  64. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +7 -3
  65. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  66. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  67. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +5 -1
  68. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  69. package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts +12 -0
  70. package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts.map +1 -0
  71. package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts +30 -0
  72. package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts.map +1 -0
  73. package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts +19 -0
  74. package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts.map +1 -0
  75. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +0 -1
  76. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
  77. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +7 -0
  78. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
  79. package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts +7 -0
  80. package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts.map +1 -0
  81. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts +17 -0
  82. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts.map +1 -0
  83. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts +12 -0
  84. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts.map +1 -0
  85. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts +25 -0
  86. package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts.map +1 -0
  87. package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts +16 -0
  88. package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts.map +1 -0
  89. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  90. package/lib/typescript/src/screens/chat-detail/types.d.ts +8 -0
  91. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  92. package/lib/typescript/src/store/message.d.ts.map +1 -1
  93. package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts +33 -0
  94. package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts.map +1 -0
  95. package/lib/typescript/src/utils/perf-marker.d.ts +8 -0
  96. package/lib/typescript/src/utils/perf-marker.d.ts.map +1 -0
  97. package/package.json +2 -1
  98. package/src/components/ThreadCard/ThreadCard.tsx +2 -0
  99. package/src/components/messages/fileMessage/index.tsx +17 -3
  100. package/src/components/messages/quotedMessage/index.tsx +5 -1
  101. package/src/context/TimelinePressGuardContext.tsx +15 -0
  102. package/src/hooks/useConversationList.ts +2 -0
  103. package/src/hooks/useDismissKeyboardFirst.ts +19 -0
  104. package/src/hooks/useExpandableText.ts +5 -1
  105. package/src/hooks/useGuardedPress.ts +20 -0
  106. package/src/screens/chat-detail/ChatComposer.tsx +105 -631
  107. package/src/screens/chat-detail/ChatDetail.tsx +38 -20
  108. package/src/screens/chat-detail/ChatListLegend.tsx +122 -71
  109. package/src/screens/chat-detail/components/ChatComposerAccessorySlot.tsx +61 -0
  110. package/src/screens/chat-detail/components/ChatComposerInputArea.tsx +198 -0
  111. package/src/screens/chat-detail/components/ChatComposerStatusBanner.tsx +116 -0
  112. package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +0 -29
  113. package/src/screens/chat-detail/hooks/useChatComposerState.ts +25 -2
  114. package/src/screens/chat-detail/hooks/useComposerBotMenu.tsx +140 -0
  115. package/src/screens/chat-detail/hooks/useComposerPanelActions.ts +57 -0
  116. package/src/screens/chat-detail/hooks/useComposerPanelDismiss.ts +111 -0
  117. package/src/screens/chat-detail/hooks/useComposerPanels.ts +197 -0
  118. package/src/screens/chat-detail/hooks/useComposerSend.ts +98 -0
  119. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +2 -0
  120. package/src/screens/chat-detail/types.ts +9 -0
  121. package/src/store/message.ts +14 -0
  122. package/src/utils/composerPanelDismissGesture.ts +72 -0
  123. package/src/utils/perf-marker.ts +39 -0
@@ -0,0 +1,116 @@
1
+ import { KContainer, KColors, KImage, KButton, KLabel } from '@droppii/libs';
2
+
3
+ import { trans } from '../../../translation';
4
+
5
+ export type ComposerStatusVariant =
6
+ | 'sellerInactive'
7
+ | 'botStart'
8
+ | 'notInGroup'
9
+ | 'sendDenied';
10
+
11
+ // Priority order matters — mirrors the composer's original if/else chain.
12
+ export const getComposerStatusVariant = ({
13
+ isDisableCompose,
14
+ isGroupConversation,
15
+ isNewBotConversation,
16
+ isNotInGroup,
17
+ isSendMessageAllowed,
18
+ }: {
19
+ isDisableCompose: boolean;
20
+ isGroupConversation: boolean;
21
+ isNewBotConversation: boolean;
22
+ isNotInGroup: boolean;
23
+ isSendMessageAllowed: boolean;
24
+ }): ComposerStatusVariant | null => {
25
+ if (isDisableCompose && !isGroupConversation) return 'sellerInactive';
26
+ if (isNewBotConversation) return 'botStart';
27
+ if (isNotInGroup) return 'notInGroup';
28
+ if (!isSendMessageAllowed) return 'sendDenied';
29
+ return null;
30
+ };
31
+
32
+ interface ChatComposerStatusBannerProps {
33
+ variant: ComposerStatusVariant;
34
+ isRunFlowPending?: boolean;
35
+ onStartConversationFlow?: () => void;
36
+ isJoiningGroup?: boolean;
37
+ onPressJoinGroup?: () => void;
38
+ sendDeniedReason?: string;
39
+ }
40
+
41
+ const InfoLine = ({ text }: { text?: string }) => (
42
+ <>
43
+ <KContainer.View marginR="0.25rem">
44
+ <KImage.VectorIcons
45
+ name="info-circle-o"
46
+ size={16}
47
+ color={KColors.palette.gray.w600}
48
+ />
49
+ </KContainer.View>
50
+ <KLabel.Text typo="TextXsNormal">{text}</KLabel.Text>
51
+ </>
52
+ );
53
+
54
+ // The composer's non-input states — rendered in place of the input row.
55
+ export const ChatComposerStatusBanner = ({
56
+ variant,
57
+ isRunFlowPending,
58
+ onStartConversationFlow,
59
+ isJoiningGroup = false,
60
+ onPressJoinGroup,
61
+ sendDeniedReason,
62
+ }: ChatComposerStatusBannerProps) => {
63
+ switch (variant) {
64
+ case 'sellerInactive':
65
+ return (
66
+ <KContainer.View row alignItems justifyContent>
67
+ <InfoLine text={trans('chat:compose_disabled_seller_inactive')} />
68
+ </KContainer.View>
69
+ );
70
+ case 'botStart':
71
+ return (
72
+ <KContainer.View padding={'1.5rem'} paddingB={0}>
73
+ <KButton.Solid
74
+ size="md"
75
+ label={trans('start')}
76
+ stretch
77
+ icon={{ vectorName: 'chat-square-o', size: 20 }}
78
+ loading={isRunFlowPending}
79
+ onPress={onStartConversationFlow}
80
+ />
81
+ </KContainer.View>
82
+ );
83
+ case 'notInGroup':
84
+ return (
85
+ <KContainer.View
86
+ paddingH="0.75rem"
87
+ justifyContent="center"
88
+ brTW={1}
89
+ brTC={KColors.border.light}
90
+ >
91
+ <KLabel.Text
92
+ typo="TextSmNormal"
93
+ textAlign="center"
94
+ paddingV={'0.75rem'}
95
+ >
96
+ {trans('group:not_in_group')}
97
+ </KLabel.Text>
98
+ <KButton.Solid
99
+ size="md"
100
+ label={trans('group:join')}
101
+ stretch
102
+ loading={isJoiningGroup}
103
+ onPress={onPressJoinGroup}
104
+ />
105
+ </KContainer.View>
106
+ );
107
+ case 'sendDenied':
108
+ return (
109
+ <KContainer.View row alignItems justifyContent paddingT={'1rem'}>
110
+ <InfoLine text={sendDeniedReason} />
111
+ </KContainer.View>
112
+ );
113
+ default:
114
+ return null;
115
+ }
116
+ };
@@ -1,11 +1,9 @@
1
1
  import { useCallback, useEffect } from 'react';
2
- import { Keyboard } from 'react-native';
3
2
  import {
4
3
  useAnimatedStyle,
5
4
  useSharedValue,
6
5
  withTiming,
7
6
  } from 'react-native-reanimated';
8
- import { useKeyboardState } from 'react-native-keyboard-controller';
9
7
 
10
8
  const ACCESSORY_ANIMATION_MS = 250;
11
9
 
@@ -17,7 +15,6 @@ interface UseChatComposerAnimationProps {
17
15
  interface UseChatComposerAnimationReturn {
18
16
  attachmentHeightSV: any;
19
17
  accessorySlotStyle: any;
20
- toggleAttachmentVisibility: () => void;
21
18
  openAttachment: () => void;
22
19
  openWithHeight: (height: number) => void;
23
20
  closeAttachment: (speedUp?: boolean, immediate?: boolean) => void;
@@ -30,8 +27,6 @@ export const useChatComposerAnimation = ({
30
27
  const panelHeightSV = useSharedValue(panelHeight);
31
28
  const attachmentHeightSV = useSharedValue(0);
32
29
 
33
- const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
34
-
35
30
  useEffect(() => {
36
31
  panelHeightSV.value = panelHeight;
37
32
  }, [panelHeight, panelHeightSV]);
@@ -71,33 +66,9 @@ export const useChatComposerAnimation = ({
71
66
  [attachmentHeightSV, onAttachmentOpenChange]
72
67
  );
73
68
 
74
- const toggleAttachmentVisibility = useCallback(() => {
75
- // Use animated value as source of truth, not stale state
76
- const isOpen = attachmentHeightSV.value > 0;
77
-
78
- if (isOpen) {
79
- closeAttachment();
80
- return;
81
- }
82
-
83
- if (isKeyboardVisible) {
84
- Keyboard.dismiss();
85
- onAttachmentOpenChange(true);
86
- }
87
-
88
- openAttachment();
89
- }, [
90
- closeAttachment,
91
- isKeyboardVisible,
92
- onAttachmentOpenChange,
93
- openAttachment,
94
- attachmentHeightSV,
95
- ]);
96
-
97
69
  return {
98
70
  attachmentHeightSV,
99
71
  accessorySlotStyle,
100
- toggleAttachmentVisibility,
101
72
  openAttachment,
102
73
  openWithHeight,
103
74
  closeAttachment,
@@ -1,8 +1,15 @@
1
- import { useState, useMemo, useRef } from 'react';
1
+ import { useState, useMemo, useRef, useCallback, useEffect } from 'react';
2
+ import { Platform } from 'react-native';
3
+
4
+ const IS_IOS = Platform.OS === 'ios';
5
+ const INPUT_LINE_HEIGHT = 22.4;
6
+ const WRAPPED_HEIGHT_THRESHOLD = IS_IOS
7
+ ? INPUT_LINE_HEIGHT + 4
8
+ : INPUT_LINE_HEIGHT + 12;
2
9
 
3
10
  // 'mention' is used only for mutual-exclusion (closing attachment/sticker
4
11
  // when the mention picker opens) — the mention picker itself renders as its
5
- // own absolutely-positioned sibling in ChatComposer, not through this
12
+ // own absolutely-positioned sibling in ChatComposerInputArea, not through this
6
13
  // state's render slot, since it must float above the input while the
7
14
  // keyboard stays open (attachment/sticker intentionally take over the
8
15
  // keyboard area, which the mention picker must not do).
@@ -18,6 +25,9 @@ interface UseChatComposerStateReturn {
18
25
  isInputWrapped: boolean;
19
26
  setIsInputWrapped: (wrapped: boolean) => void;
20
27
  isMultilineInput: boolean;
28
+ handleInputContentSizeChange: (event: {
29
+ nativeEvent: { contentSize: { height: number } };
30
+ }) => void;
21
31
  inputRef: React.RefObject<any>;
22
32
  }
23
33
 
@@ -38,6 +48,18 @@ export const useChatComposerState = (
38
48
  return value.includes('\n') || isInputWrapped;
39
49
  }, [isInputWrapped, value]);
40
50
 
51
+ const handleInputContentSizeChange = useCallback(
52
+ (event: { nativeEvent: { contentSize: { height: number } } }) => {
53
+ const { height } = event.nativeEvent.contentSize;
54
+ setIsInputWrapped(height > WRAPPED_HEIGHT_THRESHOLD);
55
+ },
56
+ []
57
+ );
58
+
59
+ useEffect(() => {
60
+ if (!value) setIsInputWrapped(false);
61
+ }, [value]);
62
+
41
63
  return {
42
64
  activePanelType,
43
65
  setActivePanelType,
@@ -48,6 +70,7 @@ export const useChatComposerState = (
48
70
  isInputWrapped,
49
71
  setIsInputWrapped,
50
72
  isMultilineInput,
73
+ handleInputContentSizeChange,
51
74
  inputRef,
52
75
  };
53
76
  };
@@ -0,0 +1,140 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { KContainer, KColors, KLabel } from '@droppii/libs';
3
+
4
+ import { trans } from '../../../translation';
5
+ import { UIUtils } from '../../../utils/UIUtils';
6
+ import {
7
+ useCloseSupportSession,
8
+ useResetFlow,
9
+ useRunFlow,
10
+ } from '../../../hooks/flows/useFlow';
11
+ import type { InputButtonItem } from '../../../types/flows';
12
+ import { DEFAULT_BUTTON_ID, isContextButton } from '../../../utils/flows';
13
+ import { DFlowState } from '../../../types/chat';
14
+ import { useChatDetailContext } from '../../../context/ChatDetailContext';
15
+ import { openBotMenuSheet } from '../BotMenuSheet';
16
+
17
+ export const useComposerBotMenu = () => {
18
+ const {
19
+ isBotConversation,
20
+ conversationFlow,
21
+ conversationFlowMainMenu,
22
+ flowState,
23
+ } = useChatDetailContext();
24
+ const { mutate: runFlow, isPending } = useRunFlow();
25
+ const { mutate: resetFlow } = useResetFlow();
26
+ const { mutateAsync: closeSupportSession } = useCloseSupportSession();
27
+
28
+ const onStartConversationFlow = useCallback(() => {
29
+ runFlow({
30
+ groupId: conversationFlow?.flowCurrentStep?.id || '',
31
+ actionId: null,
32
+ childId: null,
33
+ plainText: trans('start'),
34
+ });
35
+ }, [runFlow, conversationFlow]);
36
+
37
+ const botMenuOptions = useMemo(() => {
38
+ const apiOptions =
39
+ conversationFlowMainMenu?.filter(
40
+ (item) => !isContextButton(item.label)
41
+ ) || [];
42
+
43
+ const hasDefaultButton = apiOptions.some(
44
+ (item) => item.id === DEFAULT_BUTTON_ID
45
+ );
46
+
47
+ if (hasDefaultButton) return apiOptions;
48
+
49
+ return [...apiOptions, { id: DEFAULT_BUTTON_ID, label: trans('restart') }];
50
+ }, [conversationFlowMainMenu]);
51
+
52
+ const hasBotMenuOptions = isBotConversation && botMenuOptions.length > 0;
53
+
54
+ const runBotMenuOption = useCallback(
55
+ (item: InputButtonItem) => {
56
+ if (item.id === DEFAULT_BUTTON_ID) {
57
+ resetFlow();
58
+ return;
59
+ }
60
+ runFlow({
61
+ actionId: item.id,
62
+ groupId: '',
63
+ childId: '',
64
+ plainText: item.label,
65
+ });
66
+ },
67
+ [runFlow, resetFlow]
68
+ );
69
+
70
+ const handleSelectBotMenuOption = useCallback(
71
+ (item: InputButtonItem) => {
72
+ if (flowState === DFlowState.CS) {
73
+ UIUtils.popup.open({
74
+ header: {
75
+ title: { text: trans('chat:end_support_session_title') },
76
+ alignment: 'center',
77
+ },
78
+ body: {
79
+ renderContent: () => (
80
+ <KContainer.View padding={'1rem'}>
81
+ <KLabel.RichText
82
+ typo="TextMdNormal"
83
+ textAlign="center"
84
+ color={KColors.gray.dark}
85
+ richTextOptions={{
86
+ i18nKey: trans('chat:end_support_session_desc'),
87
+ components: {
88
+ bold: (
89
+ <KLabel.Text
90
+ typo="TextMdBold"
91
+ color={KColors.gray.dark}
92
+ />
93
+ ),
94
+ },
95
+ }}
96
+ />
97
+ </KContainer.View>
98
+ ),
99
+ },
100
+ actions: {
101
+ buttons: [
102
+ {
103
+ label: trans('chat:end_support_session_secondary'),
104
+ kind: 'light',
105
+ onPress: () => {},
106
+ },
107
+ {
108
+ label: trans('chat:end_support_session_primary'),
109
+ kind: 'danger',
110
+ onPress: async () => {
111
+ await closeSupportSession();
112
+ runBotMenuOption(item);
113
+ },
114
+ },
115
+ ],
116
+ type: 'horizontal-button',
117
+ },
118
+ });
119
+ return;
120
+ }
121
+ runBotMenuOption(item);
122
+ },
123
+ [flowState, runBotMenuOption, closeSupportSession]
124
+ );
125
+
126
+ const handleOpenBotMenu = useCallback(() => {
127
+ if (!hasBotMenuOptions) return;
128
+ openBotMenuSheet({
129
+ options: botMenuOptions,
130
+ onSelect: handleSelectBotMenuOption,
131
+ });
132
+ }, [hasBotMenuOptions, botMenuOptions, handleSelectBotMenuOption]);
133
+
134
+ return {
135
+ hasBotMenuOptions,
136
+ handleOpenBotMenu,
137
+ onStartConversationFlow,
138
+ isRunFlowPending: isPending,
139
+ };
140
+ };
@@ -0,0 +1,57 @@
1
+ import { useMemo, useRef } from 'react';
2
+ import { debounce } from 'lodash';
3
+
4
+ import { DEFAULT_PANNEL_ACTIONS } from '../constants';
5
+ import { DChatPannelActionItemType, type DChatPannelAction } from '../types';
6
+
7
+ interface UseComposerPanelActionsProps {
8
+ pannelActions?: DChatPannelAction[];
9
+ addImages?: () => void;
10
+ addFiles?: () => void;
11
+ }
12
+
13
+ // Resolves the attachment panel's actions, wiring Gallery/Documents to
14
+ // debounced pickers unless the consumer supplied its own onPress.
15
+ export const useComposerPanelActions = ({
16
+ pannelActions,
17
+ addImages,
18
+ addFiles,
19
+ }: UseComposerPanelActionsProps) => {
20
+ const addImagesRef = useRef(addImages);
21
+ addImagesRef.current = addImages;
22
+ const debouncedImagePickerRef = useRef(
23
+ debounce(() => addImagesRef.current?.(), 300, {
24
+ leading: true,
25
+ trailing: false,
26
+ })
27
+ );
28
+
29
+ const addFilesRef = useRef(addFiles);
30
+ addFilesRef.current = addFiles;
31
+ const debouncedFilePickerRef = useRef(
32
+ debounce(() => addFilesRef.current?.(), 300, {
33
+ leading: true,
34
+ trailing: false,
35
+ })
36
+ );
37
+
38
+ return useMemo(() => {
39
+ return (pannelActions || DEFAULT_PANNEL_ACTIONS)?.map((p) => {
40
+ let onPress;
41
+ switch (p.id) {
42
+ case DChatPannelActionItemType.Gallery:
43
+ onPress = debouncedImagePickerRef.current;
44
+ break;
45
+ case DChatPannelActionItemType.Documents:
46
+ onPress = debouncedFilePickerRef.current;
47
+ break;
48
+ default:
49
+ break;
50
+ }
51
+ return {
52
+ ...p,
53
+ onPress: p.onPress || onPress,
54
+ };
55
+ });
56
+ }, [pannelActions]);
57
+ };
@@ -0,0 +1,111 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import type { GestureResponderEvent, ViewProps } from 'react-native';
3
+
4
+ import type { ComposerPanelController } from '../types';
5
+ import {
6
+ IDLE_DISMISS_TOUCH,
7
+ isPressSwallowed,
8
+ markDismissTouchDragged,
9
+ moveDismissTouch,
10
+ shouldDismissOnRelease,
11
+ startDismissTouch,
12
+ type DismissTouch,
13
+ } from '../../../utils/composerPanelDismissGesture';
14
+
15
+ export type DismissAreaTouchProps = Pick<
16
+ ViewProps,
17
+ 'onTouchStart' | 'onTouchMove' | 'onTouchEnd' | 'onTouchCancel'
18
+ >;
19
+
20
+ // Tap-outside dismiss for the composer's attachment/sticker panel
21
+ // (DROPPII-30577).
22
+ //
23
+ // While a panel is open the timeline's content is made non-hittable
24
+ // (`isComposerPanelOpen` → `pointerEvents: 'none'` on the list content), so a
25
+ // tap can never reach a bubble — JS touchables and RNGH gestures alike — yet
26
+ // the scroll view itself still receives the touch and scrolls. Tap vs drag is
27
+ // read from plain touch events on a View wrapping the list; they observe the
28
+ // touch without claiming the JS responder, which on iOS would disable the
29
+ // scroll view's pan (RCTScrollView disables it while an ancestor holds the
30
+ // responder).
31
+ //
32
+ // `pointerEvents` only lands after a React round-trip (open → effect →
33
+ // setState → list re-render), so a tap right after opening a heavy panel can
34
+ // still reach a bubble. `shouldSwallowPress` closes that gap synchronously: it
35
+ // is true for every press belonging to a touch that began with the panel
36
+ // open — while it is down and for SWALLOW_AFTER_RELEASE_MS after it lifts,
37
+ // since RNGH gesture callbacks arrive after touchEnd.
38
+ export const useComposerPanelDismiss = () => {
39
+ const panelControllerRef = useRef<ComposerPanelController | null>(null);
40
+ const touchRef = useRef<DismissTouch>(IDLE_DISMISS_TOUCH);
41
+ const swallowPressRef = useRef(false);
42
+ // null while the tracked touch is down.
43
+ const releasedAtRef = useRef<number | null>(null);
44
+ const [isComposerPanelOpen, setIsComposerPanelOpen] = useState(false);
45
+
46
+ const dismissAreaTouchProps = useMemo<DismissAreaTouchProps>(
47
+ () => ({
48
+ onTouchStart: (event: GestureResponderEvent) => {
49
+ const panelOpen = !!panelControllerRef.current?.isOpen();
50
+ // A second finger must not restart the tracked touch, but a press it
51
+ // makes while the panel is open is still swallowed.
52
+ if (event.nativeEvent.touches.length > 1) {
53
+ if (panelOpen) swallowPressRef.current = true;
54
+ return;
55
+ }
56
+ touchRef.current = startDismissTouch(
57
+ panelOpen,
58
+ event.nativeEvent.pageX,
59
+ event.nativeEvent.pageY
60
+ );
61
+ swallowPressRef.current = panelOpen;
62
+ releasedAtRef.current = null;
63
+ },
64
+ onTouchMove: (event: GestureResponderEvent) => {
65
+ touchRef.current = moveDismissTouch(
66
+ touchRef.current,
67
+ event.nativeEvent.pageX,
68
+ event.nativeEvent.pageY
69
+ );
70
+ },
71
+ onTouchEnd: (event: GestureResponderEvent) => {
72
+ if (event.nativeEvent.touches.length > 0) return;
73
+ if (shouldDismissOnRelease(touchRef.current)) {
74
+ panelControllerRef.current?.close();
75
+ }
76
+ touchRef.current = IDLE_DISMISS_TOUCH;
77
+ releasedAtRef.current = Date.now();
78
+ },
79
+ // The scroll view took the touch over (pan began) — not a tap.
80
+ onTouchCancel: () => {
81
+ touchRef.current = IDLE_DISMISS_TOUCH;
82
+ releasedAtRef.current = Date.now();
83
+ },
84
+ }),
85
+ []
86
+ );
87
+
88
+ // The list's onScrollBeginDrag — a drag signal on both platforms.
89
+ const notifyScrollBeginDrag = useCallback(() => {
90
+ touchRef.current = markDismissTouchDragged(touchRef.current);
91
+ }, []);
92
+
93
+ const shouldSwallowPress = useCallback(
94
+ () =>
95
+ isPressSwallowed(
96
+ swallowPressRef.current,
97
+ releasedAtRef.current,
98
+ Date.now()
99
+ ),
100
+ []
101
+ );
102
+
103
+ return {
104
+ panelControllerRef,
105
+ isComposerPanelOpen,
106
+ onComposerPanelOpenChange: setIsComposerPanelOpen,
107
+ dismissAreaTouchProps,
108
+ notifyScrollBeginDrag,
109
+ shouldSwallowPress,
110
+ };
111
+ };