@droppii-org/chat-mobile 0.2.107 → 0.2.109
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/lib/module/components/ThreadCard/ThreadCard.js +2 -0
- package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
- package/lib/module/components/messages/fileMessage/index.js +9 -2
- package/lib/module/components/messages/fileMessage/index.js.map +1 -1
- package/lib/module/components/messages/quotedMessage/index.js +6 -1
- package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
- package/lib/module/context/TimelinePressGuardContext.js +14 -0
- package/lib/module/context/TimelinePressGuardContext.js.map +1 -0
- package/lib/module/hooks/useConversationList.js +4 -0
- package/lib/module/hooks/useConversationList.js.map +1 -1
- package/lib/module/hooks/useDismissKeyboardFirst.js +19 -0
- package/lib/module/hooks/useDismissKeyboardFirst.js.map +1 -0
- package/lib/module/hooks/useExpandableText.js +6 -1
- package/lib/module/hooks/useExpandableText.js.map +1 -1
- package/lib/module/hooks/useGuardedPress.js +19 -0
- package/lib/module/hooks/useGuardedPress.js.map +1 -0
- package/lib/module/screens/ChatInfo/ChatInfo.js +12 -3
- package/lib/module/screens/ChatInfo/ChatInfo.js.map +1 -1
- package/lib/module/screens/ChatInfo/ChatInfoActions.js +45 -40
- package/lib/module/screens/ChatInfo/ChatInfoActions.js.map +1 -1
- package/lib/module/screens/ChatInfo/ChatInfoProfile.js +11 -7
- package/lib/module/screens/ChatInfo/ChatInfoProfile.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +89 -498
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +38 -20
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +113 -76
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js +47 -0
- package/lib/module/screens/chat-detail/components/ChatComposerAccessorySlot.js.map +1 -0
- package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js +149 -0
- package/lib/module/screens/chat-detail/components/ChatComposerInputArea.js.map +1 -0
- package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js +104 -0
- package/lib/module/screens/chat-detail/components/ChatComposerStatusBanner.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +0 -17
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +16 -2
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js +122 -0
- package/lib/module/screens/chat-detail/hooks/useComposerBotMenu.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js +46 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelActions.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js +75 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanelDismiss.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanels.js +136 -0
- package/lib/module/screens/chat-detail/hooks/useComposerPanels.js.map +1 -0
- package/lib/module/screens/chat-detail/hooks/useComposerSend.js +74 -0
- package/lib/module/screens/chat-detail/hooks/useComposerSend.js.map +1 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +2 -0
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/types.js +5 -0
- package/lib/module/screens/chat-detail/types.js.map +1 -1
- package/lib/module/store/message.js +14 -0
- package/lib/module/store/message.js.map +1 -1
- package/lib/module/utils/composerPanelDismissGesture.js +52 -0
- package/lib/module/utils/composerPanelDismissGesture.js.map +1 -0
- package/lib/module/utils/perf-marker.js +44 -0
- package/lib/module/utils/perf-marker.js.map +1 -0
- package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/context/TimelinePressGuardContext.d.ts +3 -0
- package/lib/typescript/src/context/TimelinePressGuardContext.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts +8 -0
- package/lib/typescript/src/hooks/useDismissKeyboardFirst.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useExpandableText.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useGuardedPress.d.ts +9 -0
- package/lib/typescript/src/hooks/useGuardedPress.d.ts.map +1 -0
- package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts.map +1 -1
- package/lib/typescript/src/screens/ChatInfo/ChatInfoActions.d.ts +2 -1
- package/lib/typescript/src/screens/ChatInfo/ChatInfoActions.d.ts.map +1 -1
- package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts +2 -1
- package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +7 -3
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +5 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerAccessorySlot.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts +30 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerInputArea.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts +19 -0
- package/lib/typescript/src/screens/chat-detail/components/ChatComposerStatusBanner.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +0 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +7 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts +7 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerBotMenu.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts +17 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelActions.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts +12 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanelDismiss.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts +25 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerPanels.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts +16 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useComposerSend.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts +8 -0
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/store/message.d.ts.map +1 -1
- package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts +33 -0
- package/lib/typescript/src/utils/composerPanelDismissGesture.d.ts.map +1 -0
- package/lib/typescript/src/utils/perf-marker.d.ts +8 -0
- package/lib/typescript/src/utils/perf-marker.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/components/ThreadCard/ThreadCard.tsx +2 -0
- package/src/components/messages/fileMessage/index.tsx +17 -3
- package/src/components/messages/quotedMessage/index.tsx +5 -1
- package/src/context/TimelinePressGuardContext.tsx +15 -0
- package/src/hooks/useConversationList.ts +2 -0
- package/src/hooks/useDismissKeyboardFirst.ts +19 -0
- package/src/hooks/useExpandableText.ts +5 -1
- package/src/hooks/useGuardedPress.ts +20 -0
- package/src/screens/ChatInfo/ChatInfo.tsx +10 -1
- package/src/screens/ChatInfo/ChatInfoActions.tsx +47 -39
- package/src/screens/ChatInfo/ChatInfoProfile.tsx +17 -12
- package/src/screens/chat-detail/ChatComposer.tsx +105 -631
- package/src/screens/chat-detail/ChatDetail.tsx +38 -20
- package/src/screens/chat-detail/ChatListLegend.tsx +122 -71
- package/src/screens/chat-detail/components/ChatComposerAccessorySlot.tsx +61 -0
- package/src/screens/chat-detail/components/ChatComposerInputArea.tsx +198 -0
- package/src/screens/chat-detail/components/ChatComposerStatusBanner.tsx +116 -0
- package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +0 -29
- package/src/screens/chat-detail/hooks/useChatComposerState.ts +25 -2
- package/src/screens/chat-detail/hooks/useComposerBotMenu.tsx +140 -0
- package/src/screens/chat-detail/hooks/useComposerPanelActions.ts +57 -0
- package/src/screens/chat-detail/hooks/useComposerPanelDismiss.ts +111 -0
- package/src/screens/chat-detail/hooks/useComposerPanels.ts +197 -0
- package/src/screens/chat-detail/hooks/useComposerSend.ts +98 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +2 -0
- package/src/screens/chat-detail/types.ts +9 -0
- package/src/store/message.ts +14 -0
- package/src/utils/composerPanelDismissGesture.ts +72 -0
- 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
|
|
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
|
+
};
|