@droppii-org/chat-mobile 0.2.11 → 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 (132) 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/components/messages/linkMessage/index.js +35 -7
  6. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  7. package/lib/module/config/api-endpoints.js +1 -0
  8. package/lib/module/config/api-endpoints.js.map +1 -1
  9. package/lib/module/context/ChatDetailContext.js +57 -0
  10. package/lib/module/context/ChatDetailContext.js.map +1 -0
  11. package/lib/module/hooks/flows/useCurrentConversationFlow.js +9 -0
  12. package/lib/module/hooks/flows/useCurrentConversationFlow.js.map +1 -0
  13. package/lib/module/hooks/flows/useFlow.js +57 -0
  14. package/lib/module/hooks/flows/useFlow.js.map +1 -0
  15. package/lib/module/hooks/message/useSendMessage.js +4 -2
  16. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  17. package/lib/module/hooks/query-keys.js +4 -1
  18. package/lib/module/hooks/query-keys.js.map +1 -1
  19. package/lib/module/hooks/useConversationList.js +1 -1
  20. package/lib/module/hooks/useConversationList.js.map +1 -1
  21. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +6 -4
  22. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -1
  23. package/lib/module/hooks/useLinkPreview/useLinkPreview.js +2 -2
  24. package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -1
  25. package/lib/module/screens/chat-detail/ChatComposer.js +41 -3
  26. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  27. package/lib/module/screens/chat-detail/ChatDetail.js +65 -60
  28. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  29. package/lib/module/screens/chat-detail/ChatListLegend.js +45 -10
  30. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  31. package/lib/module/screens/chat-detail/ChatQuickActions.js +12 -3
  32. package/lib/module/screens/chat-detail/ChatQuickActions.js.map +1 -1
  33. package/lib/module/screens/chat-detail/conversationHeader.utils.js +1 -1
  34. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -1
  35. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +6 -1
  36. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  37. package/lib/module/screens/inbox/Inbox.js +1 -3
  38. package/lib/module/screens/inbox/Inbox.js.map +1 -1
  39. package/lib/module/screens/inbox/MessagesTab.js +2 -1
  40. package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
  41. package/lib/module/services/endpoints.js +4 -1
  42. package/lib/module/services/endpoints.js.map +1 -1
  43. package/lib/module/translation/resources/i18n.js +3 -1
  44. package/lib/module/translation/resources/i18n.js.map +1 -1
  45. package/lib/module/types/chat.js.map +1 -1
  46. package/lib/module/types/flows.js +17 -0
  47. package/lib/module/types/flows.js.map +1 -0
  48. package/lib/module/utils/flows.js +11 -0
  49. package/lib/module/utils/flows.js.map +1 -0
  50. package/lib/module/utils/url.js +12 -1
  51. package/lib/module/utils/url.js.map +1 -1
  52. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +1 -1
  53. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  54. package/lib/typescript/src/components/flows/inputButtons/index.d.ts +9 -0
  55. package/lib/typescript/src/components/flows/inputButtons/index.d.ts.map +1 -0
  56. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +2 -1
  57. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  58. package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
  59. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  60. package/lib/typescript/src/context/ChatDetailContext.d.ts +5 -0
  61. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -0
  62. package/lib/typescript/src/hooks/flows/useCurrentConversationFlow.d.ts +3 -0
  63. package/lib/typescript/src/hooks/flows/useCurrentConversationFlow.d.ts.map +1 -0
  64. package/lib/typescript/src/hooks/flows/useFlow.d.ts +4 -0
  65. package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -0
  66. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +3 -1
  67. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  68. package/lib/typescript/src/hooks/query-keys.d.ts +3 -0
  69. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  70. package/lib/typescript/src/hooks/useConversationList.d.ts +2 -2
  71. package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
  72. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts +6 -2
  73. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts.map +1 -1
  74. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts +6 -1
  75. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -1
  76. package/lib/typescript/src/index.d.ts +1 -0
  77. package/lib/typescript/src/index.d.ts.map +1 -1
  78. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +1 -1
  79. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  80. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  81. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  82. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
  83. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  84. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts.map +1 -1
  85. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -1
  86. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
  87. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  88. package/lib/typescript/src/screens/chat-detail/types.d.ts +5 -10
  89. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  90. package/lib/typescript/src/screens/inbox/Inbox.d.ts +1 -2
  91. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  92. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts +1 -2
  93. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  94. package/lib/typescript/src/services/endpoints.d.ts +3 -0
  95. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  96. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  97. package/lib/typescript/src/types/chat.d.ts +11 -1
  98. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  99. package/lib/typescript/src/types/flows.d.ts +82 -0
  100. package/lib/typescript/src/types/flows.d.ts.map +1 -0
  101. package/lib/typescript/src/utils/flows.d.ts +4 -0
  102. package/lib/typescript/src/utils/flows.d.ts.map +1 -0
  103. package/lib/typescript/src/utils/url.d.ts.map +1 -1
  104. package/package.json +6 -4
  105. package/src/components/ThreadCard/AvatarSection.tsx +2 -2
  106. package/src/components/flows/inputButtons/index.tsx +105 -0
  107. package/src/components/messages/linkMessage/index.tsx +48 -8
  108. package/src/config/api-endpoints.ts +2 -0
  109. package/src/context/ChatDetailContext.tsx +85 -0
  110. package/src/hooks/flows/useCurrentConversationFlow.ts +11 -0
  111. package/src/hooks/flows/useFlow.ts +63 -0
  112. package/src/hooks/message/useSendMessage.ts +4 -0
  113. package/src/hooks/query-keys.ts +6 -0
  114. package/src/hooks/useConversationList.ts +15 -7
  115. package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +7 -3
  116. package/src/hooks/useLinkPreview/useLinkPreview.ts +2 -2
  117. package/src/index.tsx +1 -0
  118. package/src/screens/chat-detail/ChatComposer.tsx +41 -2
  119. package/src/screens/chat-detail/ChatDetail.tsx +81 -74
  120. package/src/screens/chat-detail/ChatListLegend.tsx +57 -3
  121. package/src/screens/chat-detail/ChatQuickActions.tsx +10 -3
  122. package/src/screens/chat-detail/conversationHeader.utils.ts +1 -4
  123. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +7 -0
  124. package/src/screens/chat-detail/types.ts +9 -10
  125. package/src/screens/inbox/Inbox.tsx +3 -15
  126. package/src/screens/inbox/MessagesTab.tsx +48 -49
  127. package/src/services/endpoints.ts +6 -0
  128. package/src/translation/resources/i18n.ts +2 -0
  129. package/src/types/chat.ts +16 -1
  130. package/src/types/flows.ts +102 -0
  131. package/src/utils/flows.ts +18 -0
  132. package/src/utils/url.ts +30 -1
@@ -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';
@@ -11,8 +11,12 @@ export const useFetchUrlMetadata = (url?: string) =>
11
11
  queryFn: async () => {
12
12
  const path = `${ENDPOINTS.chatService.urlMetadata}?url=${url}`;
13
13
  const res = await apiInstance?.get<BaseResponse<IUrlMetadata>>(path);
14
- return res?.data?.data;
14
+ return res?.data?.data?.title
15
+ ? {
16
+ ...res?.data?.data,
17
+ url,
18
+ }
19
+ : null;
15
20
  },
16
21
  enabled: !!url,
17
- placeholderData: keepPreviousData,
18
22
  });
@@ -25,7 +25,7 @@ export function useLinkPreview(value: string | undefined) {
25
25
 
26
26
  const dismiss = useCallback(() => setIsDismissed(true), []);
27
27
 
28
- const isVisible = !!detectedUrl && !isDismissed;
28
+ const isVisible = !!detectedUrl && !isDismissed && (isLoading || !!metadata);
29
29
 
30
- return { isVisible, metadata, isLoading, dismiss };
30
+ return { isVisible, metadata: metadata || undefined, isLoading, dismiss };
31
31
  }
package/src/index.tsx CHANGED
@@ -48,3 +48,4 @@ export {
48
48
  mapOpenIMMessageToGiftedChat,
49
49
  mapOpenIMMessagesToGiftedChat,
50
50
  } from './utils/giftedChatMessage';
51
+ export type { PeerType } from '@droppii/openim-rn-client-sdk';
@@ -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,
@@ -137,7 +145,8 @@ export const ChatComposer = memo(
137
145
  const hasAttachments = selectedImages.length > 0;
138
146
  const hasText = (value ?? '').trim().length > 0;
139
147
  // Only send metadata if preview is CURRENTLY visible (user didn't dismiss it)
140
- const previewMetadata = showLinkPreview ? urlMetadata : undefined;
148
+ const previewMetadata =
149
+ showLinkPreview && !!urlMetadata ? urlMetadata : undefined;
141
150
  const hasPreview = !!previewMetadata;
142
151
 
143
152
  // Prevent sending while files are uploading
@@ -206,6 +215,15 @@ export const ChatComposer = memo(
206
215
  [setIsInputWrapped]
207
216
  );
208
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
+
209
227
  // Reset wrapped state when value is cleared
210
228
  useEffect(() => {
211
229
  if (!value) {
@@ -233,6 +251,27 @@ export const ChatComposer = memo(
233
251
  [insets.bottom, isAttachmentOpen]
234
252
  );
235
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
+ }
236
275
  return (
237
276
  <KContainer.View background={'rgba(131, 137, 157, 0.05)'}>
238
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
  ({
@@ -53,7 +54,6 @@ const ChatDetail = memo(
53
54
  renderChat,
54
55
  showQuickActions = true,
55
56
  quickActions,
56
- inputValue,
57
57
  inputPlaceholder,
58
58
  onChangeInput,
59
59
  onPressAttach,
@@ -65,6 +65,7 @@ const ChatDetail = memo(
65
65
  renderQuickActions,
66
66
  renderAttachmentAction,
67
67
  attachmentColumns,
68
+ onPressUrl,
68
69
  }: ChatDetailProps) => {
69
70
  const navigation = useNavigation();
70
71
  const [internalInput, setInternalInput] = useState<string>('');
@@ -154,7 +155,7 @@ const ChatDetail = memo(
154
155
  return getConversationSubtitle(conversation);
155
156
  }, [conversation, getSubtitle, subtitle]);
156
157
 
157
- const currentInput = inputValue ?? internalInput;
158
+ const currentInput = internalInput;
158
159
 
159
160
  const handleChangeText = useCallback(
160
161
  (text?: string) => {
@@ -222,83 +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
- />
267
-
268
- <ChatComposer
269
- value={currentInput}
270
- placeholder={inputPlaceholder}
271
- onChangeText={handleChangeText}
272
- onSend={handleSend}
273
- onMessageSent={handleAttachmentMessageSent}
274
- onPressAttach={
275
- 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]'))
276
239
  }
277
- onPressEmoji={onPressEmoji ?? (() => console.log('[onPressEmoji]'))}
278
- showQuickActions={showQuickActions}
279
- quickActions={quickActions ?? quickActionsDefault}
280
- attachmentActions={attachmentActions ?? attachmentActionsDefault}
281
- onQuickActionPress={
282
- onQuickActionPress ??
283
- ((action: DChatQuickAction) => {
284
- console.log('[onQuickActionPress]', action.id, action.meta);
285
- })
240
+ onPressAddMember={
241
+ onPressAddMember ?? (() => console.log('[onPressAddMember]'))
286
242
  }
287
- onAttachmentAction={
288
- onAttachmentAction ??
289
- ((action: DChatAttachmentAction) => {
290
- console.log('[onAttachmentAction]', action.id, action.meta);
291
- })
243
+ onPressMenu={onPressMenu ?? (() => console.log('[onPressMenu]'))}
244
+ onPressAvatar={
245
+ onPressAvatar ?? (() => console.log('[onPressAvatar]'))
292
246
  }
293
- renderQuickAction={renderQuickAction}
294
- renderQuickActions={renderQuickActions}
295
- renderAttachmentAction={renderAttachmentAction}
296
- attachmentColumns={attachmentColumns}
297
247
  />
298
- </KeyboardAvoidingView>
299
248
 
300
- <MediaViewerModal ref={mediaViewerRef} />
301
- </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>
302
309
  );
303
310
  }
304
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
  ({
@@ -27,6 +32,7 @@ export const ChatListLegend = memo(
27
32
  isLoadingNewer,
28
33
  hasMoreNewer,
29
34
  onMediaPress,
35
+ onPressUrl,
30
36
  }: ChatListProps) => {
31
37
  // Precompute all message data in single pass (O(n))
32
38
  const messageDataMap = useMemo(
@@ -34,6 +40,15 @@ export const ChatListLegend = memo(
34
40
  [messages]
35
41
  );
36
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
+
37
52
  const listRef = useRef<LegendListRef>(null);
38
53
  const buttonOpacity = useSharedValue(0);
39
54
  const isAtBottomRef = useRef(true);
@@ -70,6 +85,17 @@ export const ChatListLegend = memo(
70
85
  []
71
86
  );
72
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
+
73
99
  const renderItem = useCallback(
74
100
  ({ item, index }: any) => {
75
101
  try {
@@ -77,7 +103,9 @@ export const ChatListLegend = memo(
77
103
 
78
104
  const message = item as DMessageItem;
79
105
  const isOutgoing = message.sendID === currentUserId;
80
- // Use same fallback as precomputeLegendListData
106
+ const isLastIncoming =
107
+ !isOutgoing &&
108
+ (message.clientMsgID || message.serverMsgID) === lastMessageId;
81
109
  const messageId =
82
110
  message.clientMsgID ||
83
111
  message.serverMsgID ||
@@ -85,11 +113,18 @@ export const ChatListLegend = memo(
85
113
 
86
114
  const messageData = messageDataMap.get(messageId);
87
115
  if (!messageData) {
88
- return null; // Shouldn't happen if logic is consistent
116
+ return null;
89
117
  }
90
118
 
91
119
  const { dayStart, createdAt } = messageData;
92
120
 
121
+ let flowInputButtonsNode = null;
122
+ if (isLastIncoming && isBotConversation) {
123
+ flowInputButtonsNode = getFlowInputButtonsNode(
124
+ conversationFlow?.flowCurrentStep
125
+ );
126
+ }
127
+
93
128
  return (
94
129
  <>
95
130
  {dayStart && <LegendChatDay createdAt={createdAt} />}
@@ -110,6 +145,14 @@ export const ChatListLegend = memo(
110
145
  isOutgoing={isOutgoing}
111
146
  createdAtTime={createdAt}
112
147
  onMediaPress={onMediaPress}
148
+ onPressUrl={onPressUrl}
149
+ />
150
+ )}
151
+ {!!flowInputButtonsNode && (
152
+ <InputButtonFlow
153
+ nodeData={flowInputButtonsNode}
154
+ onResetFlow={onResetFlow}
155
+ onRunFlow={onRunFlow}
113
156
  />
114
157
  )}
115
158
  </KContainer.View>
@@ -133,6 +176,12 @@ export const ChatListLegend = memo(
133
176
  messageDataMap,
134
177
  handleItemLayout,
135
178
  onMediaPress,
179
+ onPressUrl,
180
+ isBotConversation,
181
+ conversationFlow,
182
+ onResetFlow,
183
+ onRunFlow,
184
+ lastMessageId,
136
185
  ]
137
186
  );
138
187
 
@@ -293,7 +342,12 @@ export const ChatListLegend = memo(
293
342
  renderItem={renderItem}
294
343
  estimatedItemSize={avgItemHeightRef.current}
295
344
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
296
- recycleItems
345
+ recycleItems={!isBotConversation}
346
+ extraData={
347
+ !isBotConversation
348
+ ? `${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`
349
+ : undefined
350
+ }
297
351
  alignItemsAtEnd
298
352
  maintainVisibleContentPosition={!isLoadingEarlier}
299
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