@droppii-org/chat-mobile 0.2.12 → 0.2.13

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 (107) hide show
  1. package/lib/module/components/ThreadCard/AvatarSection.js +2 -3
  2. package/lib/module/components/ThreadCard/AvatarSection.js.map +1 -1
  3. package/lib/module/components/flows/inputButtons/index.js +77 -0
  4. package/lib/module/components/flows/inputButtons/index.js.map +1 -0
  5. package/lib/module/config/api-endpoints.js +1 -0
  6. package/lib/module/config/api-endpoints.js.map +1 -1
  7. package/lib/module/context/ChatDetailContext.js +57 -0
  8. package/lib/module/context/ChatDetailContext.js.map +1 -0
  9. package/lib/module/hooks/flows/useCurrentConversationFlow.js +9 -0
  10. package/lib/module/hooks/flows/useCurrentConversationFlow.js.map +1 -0
  11. package/lib/module/hooks/flows/useFlow.js +57 -0
  12. package/lib/module/hooks/flows/useFlow.js.map +1 -0
  13. package/lib/module/hooks/message/useSendMessage.js +4 -2
  14. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  15. package/lib/module/hooks/query-keys.js +4 -1
  16. package/lib/module/hooks/query-keys.js.map +1 -1
  17. package/lib/module/hooks/useConversationList.js +1 -1
  18. package/lib/module/hooks/useConversationList.js.map +1 -1
  19. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +2 -3
  20. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -1
  21. package/lib/module/screens/chat-detail/ChatComposer.js +40 -2
  22. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  23. package/lib/module/screens/chat-detail/ChatDetail.js +62 -58
  24. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  25. package/lib/module/screens/chat-detail/ChatListLegend.js +41 -8
  26. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  27. package/lib/module/screens/chat-detail/ChatQuickActions.js +12 -3
  28. package/lib/module/screens/chat-detail/ChatQuickActions.js.map +1 -1
  29. package/lib/module/screens/chat-detail/conversationHeader.utils.js +1 -1
  30. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
  31. package/lib/module/screens/inbox/Inbox.js +1 -3
  32. package/lib/module/screens/inbox/Inbox.js.map +1 -1
  33. package/lib/module/screens/inbox/MessagesTab.js +2 -1
  34. package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
  35. package/lib/module/services/endpoints.js +4 -1
  36. package/lib/module/services/endpoints.js.map +1 -1
  37. package/lib/module/translation/resources/i18n.js +3 -1
  38. package/lib/module/translation/resources/i18n.js.map +1 -1
  39. package/lib/module/types/chat.js.map +1 -1
  40. package/lib/module/types/flows.js +17 -0
  41. package/lib/module/types/flows.js.map +1 -0
  42. package/lib/module/utils/flows.js +11 -0
  43. package/lib/module/utils/flows.js.map +1 -0
  44. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +1 -1
  45. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  46. package/lib/typescript/src/components/flows/inputButtons/index.d.ts +9 -0
  47. package/lib/typescript/src/components/flows/inputButtons/index.d.ts.map +1 -0
  48. package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
  49. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  50. package/lib/typescript/src/context/ChatDetailContext.d.ts +5 -0
  51. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -0
  52. package/lib/typescript/src/hooks/flows/useCurrentConversationFlow.d.ts +3 -0
  53. package/lib/typescript/src/hooks/flows/useCurrentConversationFlow.d.ts.map +1 -0
  54. package/lib/typescript/src/hooks/flows/useFlow.d.ts +4 -0
  55. package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -0
  56. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +3 -1
  57. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  58. package/lib/typescript/src/hooks/query-keys.d.ts +3 -0
  59. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  60. package/lib/typescript/src/hooks/useConversationList.d.ts +2 -2
  61. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  62. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts.map +1 -1
  63. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +1 -1
  64. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  65. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  66. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  67. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts.map +1 -1
  68. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
  69. package/lib/typescript/src/screens/chat-detail/types.d.ts +1 -0
  70. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  71. package/lib/typescript/src/screens/inbox/Inbox.d.ts +1 -2
  72. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  73. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts +1 -2
  74. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  75. package/lib/typescript/src/services/endpoints.d.ts +3 -0
  76. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  77. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  78. package/lib/typescript/src/types/chat.d.ts +11 -1
  79. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  80. package/lib/typescript/src/types/flows.d.ts +82 -0
  81. package/lib/typescript/src/types/flows.d.ts.map +1 -0
  82. package/lib/typescript/src/utils/flows.d.ts +4 -0
  83. package/lib/typescript/src/utils/flows.d.ts.map +1 -0
  84. package/package.json +2 -2
  85. package/src/components/ThreadCard/AvatarSection.tsx +2 -2
  86. package/src/components/flows/inputButtons/index.tsx +105 -0
  87. package/src/config/api-endpoints.ts +2 -0
  88. package/src/context/ChatDetailContext.tsx +85 -0
  89. package/src/hooks/flows/useCurrentConversationFlow.ts +11 -0
  90. package/src/hooks/flows/useFlow.ts +63 -0
  91. package/src/hooks/message/useSendMessage.ts +4 -0
  92. package/src/hooks/query-keys.ts +6 -0
  93. package/src/hooks/useConversationList.ts +15 -7
  94. package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +1 -2
  95. package/src/screens/chat-detail/ChatComposer.tsx +39 -1
  96. package/src/screens/chat-detail/ChatDetail.tsx +79 -73
  97. package/src/screens/chat-detail/ChatListLegend.tsx +54 -3
  98. package/src/screens/chat-detail/ChatQuickActions.tsx +10 -3
  99. package/src/screens/chat-detail/conversationHeader.utils.ts +1 -4
  100. package/src/screens/chat-detail/types.ts +1 -0
  101. package/src/screens/inbox/Inbox.tsx +3 -15
  102. package/src/screens/inbox/MessagesTab.tsx +48 -49
  103. package/src/services/endpoints.ts +6 -0
  104. package/src/translation/resources/i18n.ts +2 -0
  105. package/src/types/chat.ts +16 -1
  106. package/src/types/flows.ts +102 -0
  107. package/src/utils/flows.ts +18 -0
@@ -0,0 +1,85 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ } from 'react';
9
+ import type {
10
+ ChatDetailContextType,
11
+ ChatDetailProviderProps,
12
+ } from '../types/chat';
13
+ import { fechtCurrentConversationFlow } from '../hooks/flows/useCurrentConversationFlow';
14
+ import { useConversationStore } from '../store';
15
+ import { PeerType } from '@droppii/openim-rn-client-sdk';
16
+ import type { IConversationFlow, IConversationFlowStep } from '../types/flows';
17
+
18
+ export const ChatDetailContext = createContext<ChatDetailContextType>({
19
+ isBotConversation: false,
20
+ conversationFlow: undefined,
21
+ refetchConversationFlow: () => {},
22
+ setConverationFlowStep: () => {},
23
+ });
24
+
25
+ export const useChatDetailContext = (): ChatDetailContextType => {
26
+ const context = useContext(ChatDetailContext);
27
+ if (!context) {
28
+ throw new Error(
29
+ 'useChatDetailContext must be used within ChatDetailProvider'
30
+ );
31
+ }
32
+ return context;
33
+ };
34
+
35
+ export const ChatDetailProvider = ({ children }: ChatDetailProviderProps) => {
36
+ const conversationId = useConversationStore(
37
+ (state) => state.currentConversationId || ''
38
+ );
39
+ const conversation = useConversationStore(
40
+ (state) => state.map[conversationId]
41
+ );
42
+ const [conversationFlow, setConverationFlow] = useState<
43
+ IConversationFlow | undefined
44
+ >(undefined);
45
+
46
+ const isBotConversation = useMemo(() => {
47
+ return conversation?.peerType === PeerType.Bot;
48
+ }, [conversation]);
49
+
50
+ const setConverationFlowStep = useCallback((step: IConversationFlowStep) => {
51
+ setConverationFlow(
52
+ (prev) =>
53
+ ({
54
+ ...prev,
55
+ flowCurrentStep: step,
56
+ }) as IConversationFlow
57
+ );
58
+ }, []);
59
+
60
+ const getConversationFlow = useCallback(async () => {
61
+ const res = await fechtCurrentConversationFlow(conversationId);
62
+ if (res) {
63
+ setConverationFlow(res);
64
+ }
65
+ }, [conversationId]);
66
+
67
+ useEffect(() => {
68
+ if (!!conversationId && !!isBotConversation) {
69
+ getConversationFlow();
70
+ }
71
+ }, [conversationId, isBotConversation, getConversationFlow]);
72
+
73
+ return (
74
+ <ChatDetailContext.Provider
75
+ value={{
76
+ conversationFlow,
77
+ refetchConversationFlow: () => {},
78
+ setConverationFlowStep,
79
+ isBotConversation,
80
+ }}
81
+ >
82
+ {children}
83
+ </ChatDetailContext.Provider>
84
+ );
85
+ };
@@ -0,0 +1,11 @@
1
+ import { ENDPOINTS } from '../../services/endpoints';
2
+ import type { BaseResponse } from '../../types/auth';
3
+ import { apiInstance } from '../../services/apis';
4
+ import type { IConversationFlow } from '../../types/flows';
5
+
6
+ export const fechtCurrentConversationFlow = async (conversationId: string) => {
7
+ const res = await apiInstance?.get<BaseResponse<IConversationFlow>>(
8
+ ENDPOINTS.chatService.conversationFlow(conversationId)
9
+ );
10
+ return res?.data?.data || undefined;
11
+ };
@@ -0,0 +1,63 @@
1
+ import { useMutation } from '@tanstack/react-query';
2
+ import { conversationQueryKeys } from '../query-keys';
3
+ import { useConversationStore } from '../../store';
4
+ import { apiInstance } from '../../services/apis';
5
+ import type { BaseResponse } from '../../types/auth';
6
+ import { ENDPOINTS } from '../../services/endpoints';
7
+ import type {
8
+ IConversationFlow,
9
+ IRunConversationFlowPayload,
10
+ IRunConversationFlowResponse,
11
+ } from '../../types/flows';
12
+ import { useSendMessage } from '../message/useSendMessage';
13
+ import { useChatDetailContext } from '../../context/ChatDetailContext';
14
+
15
+ export const useResetFlow = () => {
16
+ const conversationId =
17
+ useConversationStore((state) => state.currentConversationId) || '';
18
+ const { mutate: runFlow } = useRunFlow();
19
+ return useMutation({
20
+ mutationKey: conversationQueryKeys.flow(conversationId),
21
+ mutationFn: async () => {
22
+ const res = await apiInstance?.post<BaseResponse<IConversationFlow>>(
23
+ ENDPOINTS.chatService.resetConversationFlow(conversationId),
24
+ {}
25
+ );
26
+ return res?.data?.data || undefined;
27
+ },
28
+ onSuccess: (data) => {
29
+ if (data?.flowCurrentStep?.id) {
30
+ runFlow({
31
+ groupId: data?.flowCurrentStep?.id,
32
+ childId: null,
33
+ actionId: null,
34
+ });
35
+ }
36
+ },
37
+ });
38
+ };
39
+
40
+ export const useRunFlow = () => {
41
+ const conversationId =
42
+ useConversationStore((state) => state.currentConversationId) || '';
43
+ const { sendMessage } = useSendMessage();
44
+ const { setConverationFlowStep } = useChatDetailContext();
45
+ return useMutation({
46
+ mutationKey: conversationQueryKeys.runFlow(conversationId),
47
+ mutationFn: async (payload: IRunConversationFlowPayload) => {
48
+ await sendMessage({
49
+ plainText: payload?.plainText || '',
50
+ flow: payload,
51
+ });
52
+ const res = await apiInstance?.post<
53
+ BaseResponse<IRunConversationFlowResponse>
54
+ >(ENDPOINTS.chatService.runConversationFlow(conversationId), payload);
55
+ return res?.data?.data || undefined;
56
+ },
57
+ onSuccess: (data) => {
58
+ if (data?.flowNextStep) {
59
+ setConverationFlowStep(data?.flowNextStep);
60
+ }
61
+ },
62
+ });
63
+ };
@@ -13,6 +13,7 @@ import { useChatContext } from '../../context';
13
13
  import { generateContentBasedOnMessageType } from '../../utils/conversation';
14
14
  import { useMessageStore } from '../../store/message';
15
15
  import type { IUrlMetadata } from '../../types/common';
16
+ import type { IRunConversationFlowPayload } from '../../types/flows';
16
17
 
17
18
  export const useSendMessage = () => {
18
19
  const applicationType = useChatContext().applicationType;
@@ -23,10 +24,12 @@ export const useSendMessage = () => {
23
24
  plainText,
24
25
  files,
25
26
  urlMetadata,
27
+ flow,
26
28
  }: {
27
29
  plainText?: string;
28
30
  files?: UploadFileItem[]; //TODO
29
31
  urlMetadata?: IUrlMetadata;
32
+ flow?: IRunConversationFlowPayload;
30
33
  }) => {
31
34
  const { userID: recvID, groupID } =
32
35
  useConversationStore.getState().getCurrentConversation() || {};
@@ -62,6 +65,7 @@ export const useSendMessage = () => {
62
65
  const extendMessageInfo: IMessageItemEx = {
63
66
  applicationType,
64
67
  urlMetadata,
68
+ flow,
65
69
  };
66
70
 
67
71
  const successMessage = await OpenIMSDK.sendMessage({
@@ -11,6 +11,12 @@ export const conversationQueryKeys = {
11
11
  details: () => [...conversationQueryKeys.all, 'detail'] as const,
12
12
  detail: (conversationId: string) =>
13
13
  [...conversationQueryKeys.details(), conversationId] as const,
14
+ flow: (conversationId: string) => ['conversationFlow', conversationId],
15
+ resetFlow: (conversationId: string) => [
16
+ 'resetConversationFlow',
17
+ conversationId,
18
+ ],
19
+ runFlow: (conversationId: string) => ['runConversationFlow', conversationId],
14
20
  };
15
21
 
16
22
  export type ConversationQueryKeys = typeof conversationQueryKeys;
@@ -5,10 +5,14 @@ import OpenIMSDK, {
5
5
  } from '@droppii/openim-rn-client-sdk';
6
6
  import { conversationQueryKeys } from './query-keys';
7
7
  import { useConversationStore } from '../store';
8
- import type { DConversationItem, DMessageItem } from '../types/chat';
8
+ import type {
9
+ DChatApplicationType,
10
+ DConversationItem,
11
+ DMessageItem,
12
+ } from '../types/chat';
9
13
 
10
14
  type Params = {
11
- applicationType: string;
15
+ applicationType: DChatApplicationType;
12
16
  page: number;
13
17
  enabled?: boolean;
14
18
  };
@@ -23,17 +27,22 @@ function toConversationItem(raw: ConversationItem): DConversationItem {
23
27
  }
24
28
  }
25
29
 
26
- let applicationType: string | undefined;
30
+ let applicationType: DChatApplicationType | undefined;
27
31
  if (raw.ex) {
28
32
  try {
29
- applicationType = (JSON.parse(raw.ex) as { applicationType?: string })
30
- .applicationType;
33
+ applicationType = (
34
+ JSON.parse(raw.ex) as { applicationType?: DChatApplicationType }
35
+ ).applicationType;
31
36
  } catch {
32
37
  // leave undefined
33
38
  }
34
39
  }
35
40
 
36
- return { ...(raw as DConversationItem), lastMessage, applicationType };
41
+ return {
42
+ ...(raw as DConversationItem),
43
+ lastMessage,
44
+ applicationType: applicationType as DChatApplicationType,
45
+ };
37
46
  }
38
47
 
39
48
  export function useConversationList({
@@ -55,7 +64,6 @@ export function useConversationList({
55
64
  count: 20,
56
65
  applicationType,
57
66
  });
58
-
59
67
  const conversations = rawList.map(toConversationItem);
60
68
  useConversationStore.getState().updateConversations(conversations);
61
69
  return conversations;
@@ -1,4 +1,4 @@
1
- import { keepPreviousData, useQuery } from '@tanstack/react-query';
1
+ import { useQuery } from '@tanstack/react-query';
2
2
  import { commonQueryKeys } from '../query-keys';
3
3
  import { apiInstance } from '../../services/apis';
4
4
  import type { BaseResponse } from '../../types/auth';
@@ -19,5 +19,4 @@ export const useFetchUrlMetadata = (url?: string) =>
19
19
  : null;
20
20
  },
21
21
  enabled: !!url,
22
- placeholderData: keepPreviousData,
23
22
  });
@@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useMemo } from 'react';
2
2
  import { Platform, StyleSheet } from 'react-native';
3
3
  import Reanimated from 'react-native-reanimated';
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
- import { KContainer, KColors, KImage } from '@droppii/libs';
5
+ import { KContainer, KColors, KImage, KButton } from '@droppii/libs';
6
6
 
7
7
  const IS_IOS = Platform.OS === 'ios';
8
8
  const INPUT_LINE_HEIGHT = 22.4;
@@ -25,6 +25,10 @@ import { useChatComposerState } from './hooks/useChatComposerState';
25
25
  import { useChatComposerAnimation } from './hooks/useChatComposerAnimation';
26
26
  import { useAttachmentSendHandler } from './hooks/useAttachmentSendHandler';
27
27
  import { isVideoFromPicker } from '../../utils/imageUtils';
28
+ import { useChatDetailContext } from '../../context/ChatDetailContext';
29
+ import { trans } from '../../translation';
30
+ import { useRunFlow } from '../../hooks/flows/useFlow';
31
+ import { ReactFlowsNodeTypes } from '../../types/flows';
28
32
 
29
33
  export const ChatComposer = memo(
30
34
  ({
@@ -43,10 +47,14 @@ export const ChatComposer = memo(
43
47
  renderQuickActions,
44
48
  renderAttachmentAction,
45
49
  attachmentColumns,
50
+ isEmptyMessage = false,
46
51
  }: ChatComposerProps) => {
47
52
  const insets = useSafeAreaInsets();
48
53
  const hasAttachmentActions = !!attachmentActions?.length;
49
54
 
55
+ const { isBotConversation, conversationFlow } = useChatDetailContext();
56
+ const { mutate: runFlow, isPending } = useRunFlow();
57
+
50
58
  // Link preview
51
59
  const {
52
60
  isVisible: showLinkPreview,
@@ -207,6 +215,15 @@ export const ChatComposer = memo(
207
215
  [setIsInputWrapped]
208
216
  );
209
217
 
218
+ const onStartConversationFlow = useCallback(() => {
219
+ runFlow({
220
+ groupId: conversationFlow?.flowCurrentStep?.id || '',
221
+ actionId: null,
222
+ childId: null,
223
+ plainText: trans('start'),
224
+ });
225
+ }, [runFlow, conversationFlow]);
226
+
210
227
  // Reset wrapped state when value is cleared
211
228
  useEffect(() => {
212
229
  if (!value) {
@@ -234,6 +251,27 @@ export const ChatComposer = memo(
234
251
  [insets.bottom, isAttachmentOpen]
235
252
  );
236
253
 
254
+ if (
255
+ isBotConversation &&
256
+ conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
257
+ isEmptyMessage
258
+ ) {
259
+ return (
260
+ <KContainer.View padding={'1.5rem'} paddingB={0}>
261
+ <KButton.Solid
262
+ size="md"
263
+ label={trans('start')}
264
+ stretch
265
+ icon={{
266
+ vectorName: 'chat-square-o',
267
+ size: 20,
268
+ }}
269
+ loading={isPending}
270
+ onPress={onStartConversationFlow}
271
+ />
272
+ </KContainer.View>
273
+ );
274
+ }
237
275
  return (
238
276
  <KContainer.View background={'rgba(131, 137, 157, 0.05)'}>
239
277
  <ChatQuickActions
@@ -32,6 +32,7 @@ import type {
32
32
  import { useSendMessage } from '../../hooks/message/useSendMessage';
33
33
  import type { IUrlMetadata } from '../../types/common';
34
34
  import { useMessageStore } from '../../store/message';
35
+ import { ChatDetailProvider } from '../../context/ChatDetailContext';
35
36
 
36
37
  const ChatDetail = memo(
37
38
  ({
@@ -222,84 +223,89 @@ const ChatDetail = memo(
222
223
  const insets = useSafeAreaInsets();
223
224
 
224
225
  return (
225
- <KContainer.Page flex edges={['bottom']}>
226
- <ChatDetailHeader
227
- title={resolvedTitle}
228
- subtitle={resolvedSubtitle}
229
- avatarUri={resolvedAvatarUri}
230
- avatarFullName={resolvedAvatarFullName}
231
- peerType={resolvedPeerType}
232
- applicationType={resolvedApplicationType}
233
- showAddMember={resolvedShowAddMember}
234
- onBack={onBack ?? handleBackDefault}
235
- onPressSearch={
236
- onPressSearch ?? (() => console.log('[onPressSearch]'))
237
- }
238
- onPressAddMember={
239
- onPressAddMember ?? (() => console.log('[onPressAddMember]'))
240
- }
241
- onPressMenu={onPressMenu ?? (() => console.log('[onPressMenu]'))}
242
- onPressAvatar={
243
- onPressAvatar ?? (() => console.log('[onPressAvatar]'))
244
- }
245
- />
246
-
247
- <KeyboardAvoidingView
248
- style={styles.keyboardAvoiding}
249
- behavior="padding"
250
- keyboardVerticalOffset={Platform.select({
251
- android: insets.bottom,
252
- default: 0,
253
- })}
254
- >
255
- <ChatListLegend
256
- messages={messages}
257
- currentUserId={currentUserId}
258
- renderChat={renderChat}
259
- onLoadEarlier={onLoadEarlier}
260
- isLoading={isLoading}
261
- isLoadingEarlier={isLoadingEarlier}
262
- hasMoreEarlier={hasMoreEarlier}
263
- onMediaPress={(items, index) => {
264
- mediaViewerRef.current?.show(items, index);
265
- }}
266
- onPressUrl={onPressUrl}
267
- />
268
-
269
- <ChatComposer
270
- value={currentInput}
271
- placeholder={inputPlaceholder}
272
- onChangeText={handleChangeText}
273
- onSend={handleSend}
274
- onMessageSent={handleAttachmentMessageSent}
275
- onPressAttach={
276
- onPressAttach ?? (() => console.log('[onPressAttach]'))
226
+ <ChatDetailProvider>
227
+ <KContainer.Page flex edges={['bottom']}>
228
+ <ChatDetailHeader
229
+ title={resolvedTitle}
230
+ subtitle={resolvedSubtitle}
231
+ avatarUri={resolvedAvatarUri}
232
+ avatarFullName={resolvedAvatarFullName}
233
+ peerType={resolvedPeerType}
234
+ applicationType={resolvedApplicationType}
235
+ showAddMember={resolvedShowAddMember}
236
+ onBack={onBack ?? handleBackDefault}
237
+ onPressSearch={
238
+ onPressSearch ?? (() => console.log('[onPressSearch]'))
277
239
  }
278
- onPressEmoji={onPressEmoji ?? (() => console.log('[onPressEmoji]'))}
279
- showQuickActions={showQuickActions}
280
- quickActions={quickActions ?? quickActionsDefault}
281
- attachmentActions={attachmentActions ?? attachmentActionsDefault}
282
- onQuickActionPress={
283
- onQuickActionPress ??
284
- ((action: DChatQuickAction) => {
285
- console.log('[onQuickActionPress]', action.id, action.meta);
286
- })
240
+ onPressAddMember={
241
+ onPressAddMember ?? (() => console.log('[onPressAddMember]'))
287
242
  }
288
- onAttachmentAction={
289
- onAttachmentAction ??
290
- ((action: DChatAttachmentAction) => {
291
- console.log('[onAttachmentAction]', action.id, action.meta);
292
- })
243
+ onPressMenu={onPressMenu ?? (() => console.log('[onPressMenu]'))}
244
+ onPressAvatar={
245
+ onPressAvatar ?? (() => console.log('[onPressAvatar]'))
293
246
  }
294
- renderQuickAction={renderQuickAction}
295
- renderQuickActions={renderQuickActions}
296
- renderAttachmentAction={renderAttachmentAction}
297
- attachmentColumns={attachmentColumns}
298
247
  />
299
- </KeyboardAvoidingView>
300
248
 
301
- <MediaViewerModal ref={mediaViewerRef} />
302
- </KContainer.Page>
249
+ <KeyboardAvoidingView
250
+ style={styles.keyboardAvoiding}
251
+ behavior="padding"
252
+ keyboardVerticalOffset={Platform.select({
253
+ android: insets.bottom,
254
+ default: 0,
255
+ })}
256
+ >
257
+ <ChatListLegend
258
+ messages={messages}
259
+ currentUserId={currentUserId}
260
+ renderChat={renderChat}
261
+ onLoadEarlier={onLoadEarlier}
262
+ isLoading={isLoading}
263
+ isLoadingEarlier={isLoadingEarlier}
264
+ hasMoreEarlier={hasMoreEarlier}
265
+ onMediaPress={(items, index) => {
266
+ mediaViewerRef.current?.show(items, index);
267
+ }}
268
+ onPressUrl={onPressUrl}
269
+ />
270
+
271
+ <ChatComposer
272
+ value={currentInput}
273
+ placeholder={inputPlaceholder}
274
+ onChangeText={handleChangeText}
275
+ onSend={handleSend}
276
+ onMessageSent={handleAttachmentMessageSent}
277
+ onPressAttach={
278
+ onPressAttach ?? (() => console.log('[onPressAttach]'))
279
+ }
280
+ onPressEmoji={
281
+ onPressEmoji ?? (() => console.log('[onPressEmoji]'))
282
+ }
283
+ showQuickActions={showQuickActions}
284
+ quickActions={quickActions ?? quickActionsDefault}
285
+ attachmentActions={attachmentActions ?? attachmentActionsDefault}
286
+ onQuickActionPress={
287
+ onQuickActionPress ??
288
+ ((action: DChatQuickAction) => {
289
+ console.log('[onQuickActionPress]', action.id, action.meta);
290
+ })
291
+ }
292
+ onAttachmentAction={
293
+ onAttachmentAction ??
294
+ ((action: DChatAttachmentAction) => {
295
+ console.log('[onAttachmentAction]', action.id, action.meta);
296
+ })
297
+ }
298
+ renderQuickAction={renderQuickAction}
299
+ renderQuickActions={renderQuickActions}
300
+ renderAttachmentAction={renderAttachmentAction}
301
+ attachmentColumns={attachmentColumns}
302
+ isEmptyMessage={messages?.length === 0}
303
+ />
304
+ </KeyboardAvoidingView>
305
+
306
+ <MediaViewerModal ref={mediaViewerRef} />
307
+ </KContainer.Page>
308
+ </ChatDetailProvider>
303
309
  );
304
310
  }
305
311
  );
@@ -13,6 +13,11 @@ import type { DMessageItem } from '../../types/chat';
13
13
  import { LegendChatMessage } from './legend/LegendChatMessage';
14
14
  import { LegendChatDay } from './legend/LegendChatDay';
15
15
  import { LegendChatLoadEarlier } from './legend/LegendChatLoadEarlier';
16
+ import { useChatDetailContext } from '../../context/ChatDetailContext';
17
+ import { getFlowInputButtonsNode } from '../../utils/flows';
18
+ import { InputButtonFlow } from '../../components/flows/inputButtons';
19
+ import { useResetFlow, useRunFlow } from '../../hooks/flows/useFlow';
20
+ import type { IRunConversationFlowPayload } from '../../types/flows';
16
21
 
17
22
  export const ChatListLegend = memo(
18
23
  ({
@@ -35,6 +40,15 @@ export const ChatListLegend = memo(
35
40
  [messages]
36
41
  );
37
42
 
43
+ const { isBotConversation, conversationFlow } = useChatDetailContext();
44
+
45
+ const lastMessageId = useMemo(() => {
46
+ const last = messages[messages.length - 1];
47
+ return last?.clientMsgID || last?.serverMsgID || null;
48
+ }, [messages]);
49
+ const { mutate: runFlow } = useRunFlow();
50
+ const { mutate: resetFlow } = useResetFlow();
51
+
38
52
  const listRef = useRef<LegendListRef>(null);
39
53
  const buttonOpacity = useSharedValue(0);
40
54
  const isAtBottomRef = useRef(true);
@@ -71,6 +85,17 @@ export const ChatListLegend = memo(
71
85
  []
72
86
  );
73
87
 
88
+ const onRunFlow = useCallback(
89
+ (payload: IRunConversationFlowPayload) => {
90
+ runFlow(payload);
91
+ },
92
+ [runFlow]
93
+ );
94
+
95
+ const onResetFlow = useCallback(() => {
96
+ resetFlow();
97
+ }, [resetFlow]);
98
+
74
99
  const renderItem = useCallback(
75
100
  ({ item, index }: any) => {
76
101
  try {
@@ -78,7 +103,9 @@ export const ChatListLegend = memo(
78
103
 
79
104
  const message = item as DMessageItem;
80
105
  const isOutgoing = message.sendID === currentUserId;
81
- // Use same fallback as precomputeLegendListData
106
+ const isLastIncoming =
107
+ !isOutgoing &&
108
+ (message.clientMsgID || message.serverMsgID) === lastMessageId;
82
109
  const messageId =
83
110
  message.clientMsgID ||
84
111
  message.serverMsgID ||
@@ -86,11 +113,18 @@ export const ChatListLegend = memo(
86
113
 
87
114
  const messageData = messageDataMap.get(messageId);
88
115
  if (!messageData) {
89
- return null; // Shouldn't happen if logic is consistent
116
+ return null;
90
117
  }
91
118
 
92
119
  const { dayStart, createdAt } = messageData;
93
120
 
121
+ let flowInputButtonsNode = null;
122
+ if (isLastIncoming && isBotConversation) {
123
+ flowInputButtonsNode = getFlowInputButtonsNode(
124
+ conversationFlow?.flowCurrentStep
125
+ );
126
+ }
127
+
94
128
  return (
95
129
  <>
96
130
  {dayStart && <LegendChatDay createdAt={createdAt} />}
@@ -114,6 +148,13 @@ export const ChatListLegend = memo(
114
148
  onPressUrl={onPressUrl}
115
149
  />
116
150
  )}
151
+ {!!flowInputButtonsNode && (
152
+ <InputButtonFlow
153
+ nodeData={flowInputButtonsNode}
154
+ onResetFlow={onResetFlow}
155
+ onRunFlow={onRunFlow}
156
+ />
157
+ )}
117
158
  </KContainer.View>
118
159
  </>
119
160
  );
@@ -136,6 +177,11 @@ export const ChatListLegend = memo(
136
177
  handleItemLayout,
137
178
  onMediaPress,
138
179
  onPressUrl,
180
+ isBotConversation,
181
+ conversationFlow,
182
+ onResetFlow,
183
+ onRunFlow,
184
+ lastMessageId,
139
185
  ]
140
186
  );
141
187
 
@@ -296,7 +342,12 @@ export const ChatListLegend = memo(
296
342
  renderItem={renderItem}
297
343
  estimatedItemSize={avgItemHeightRef.current}
298
344
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
299
- recycleItems
345
+ recycleItems={!isBotConversation}
346
+ extraData={
347
+ !isBotConversation
348
+ ? `${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`
349
+ : undefined
350
+ }
300
351
  alignItemsAtEnd
301
352
  maintainVisibleContentPosition={!isLoadingEarlier}
302
353
  maintainScrollAtEnd={maintainScrollConfig}
@@ -9,6 +9,7 @@ import {
9
9
  } from '@droppii/libs';
10
10
  import { useChatActionPress } from './useChatActionPress';
11
11
  import type { DChatQuickAction, ChatQuickActionsProps } from './types';
12
+ import { useResetFlow } from '../../hooks/flows/useFlow';
12
13
 
13
14
  const QuickActionItem = memo(
14
15
  ({
@@ -69,18 +70,24 @@ export const ChatQuickActions = memo(
69
70
  renderQuickActions,
70
71
  }: ChatQuickActionsProps) => {
71
72
  const handleActionPress = useChatActionPress(onActionPress);
73
+ const { mutate: resetFlow } = useResetFlow();
72
74
 
73
75
  const renderItem = useCallback<ListRenderItem<DChatQuickAction>>(
74
76
  ({ item }) => {
75
- const onPress = () => handleActionPress(item);
77
+ const onPress = () => {
78
+ handleActionPress(item);
79
+ if (item.id === 'restart') {
80
+ resetFlow?.();
81
+ }
82
+ };
76
83
 
77
84
  if (renderAction) {
78
85
  return <>{renderAction(item, onPress)}</>;
79
86
  }
80
87
 
81
- return <QuickActionItem action={item} onPress={handleActionPress} />;
88
+ return <QuickActionItem action={item} onPress={onPress} />;
82
89
  },
83
- [handleActionPress, renderAction]
90
+ [handleActionPress, renderAction, resetFlow]
84
91
  );
85
92
 
86
93
  const keyExtractor = useCallback((item: DChatQuickAction) => item.id, []);
@@ -32,10 +32,7 @@ export const getConversationSubtitle = (
32
32
  return `20 thành viên • ${unreadLabel}`;
33
33
  }
34
34
 
35
- if (
36
- conversation.peerType === PeerType.Customer ||
37
- conversation.applicationType === 'MALL'
38
- ) {
35
+ if (conversation.peerType === PeerType.Customer) {
39
36
  return '338,5K Đã bán • 4,2K Theo dõi';
40
37
  }
41
38