@droppii-org/chat-mobile 0.2.51 → 0.2.53

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 (162) hide show
  1. package/lib/module/assets/images/index.js +2 -1
  2. package/lib/module/assets/images/index.js.map +1 -1
  3. package/lib/module/assets/images/resource-center-empty-box.png +0 -0
  4. package/lib/module/components/MediaViewerModal.js +2 -1
  5. package/lib/module/components/MediaViewerModal.js.map +1 -1
  6. package/lib/module/components/MergedImageGrid.js +1 -0
  7. package/lib/module/components/MergedImageGrid.js.map +1 -1
  8. package/lib/module/config/api-endpoints.js +2 -1
  9. package/lib/module/config/api-endpoints.js.map +1 -1
  10. package/lib/module/context/ChatDetailContext.js +2 -1
  11. package/lib/module/context/ChatDetailContext.js.map +1 -1
  12. package/lib/module/core/notification/pushToken.js +7 -7
  13. package/lib/module/core/notification/pushToken.js.map +1 -1
  14. package/lib/module/hooks/flows/useFlow.js +10 -0
  15. package/lib/module/hooks/flows/useFlow.js.map +1 -1
  16. package/lib/module/hooks/query-keys.js +5 -0
  17. package/lib/module/hooks/query-keys.js.map +1 -1
  18. package/lib/module/hooks/resourceCenter/useThreadResources.js +116 -0
  19. package/lib/module/hooks/resourceCenter/useThreadResources.js.map +1 -0
  20. package/lib/module/hooks/useImageAttachment.js +0 -2
  21. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  22. package/lib/module/hooks/useMediaViewer.js +4 -1
  23. package/lib/module/hooks/useMediaViewer.js.map +1 -1
  24. package/lib/module/hooks/useSendAttachment.js +4 -1
  25. package/lib/module/hooks/useSendAttachment.js.map +1 -1
  26. package/lib/module/index.js +1 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/screens/MediaView/index.js +104 -42
  29. package/lib/module/screens/MediaView/index.js.map +1 -1
  30. package/lib/module/screens/ResourceCenter/FileResourceRow.js +80 -0
  31. package/lib/module/screens/ResourceCenter/FileResourceRow.js.map +1 -0
  32. package/lib/module/screens/ResourceCenter/GridRenderer.js +83 -0
  33. package/lib/module/screens/ResourceCenter/GridRenderer.js.map +1 -0
  34. package/lib/module/screens/ResourceCenter/GridThumbnail.js +97 -0
  35. package/lib/module/screens/ResourceCenter/GridThumbnail.js.map +1 -0
  36. package/lib/module/screens/ResourceCenter/LinkResourceRow.js +81 -0
  37. package/lib/module/screens/ResourceCenter/LinkResourceRow.js.map +1 -0
  38. package/lib/module/screens/ResourceCenter/ListRenderer.js +60 -0
  39. package/lib/module/screens/ResourceCenter/ListRenderer.js.map +1 -0
  40. package/lib/module/screens/ResourceCenter/ResourceCenterTabBar.js +75 -0
  41. package/lib/module/screens/ResourceCenter/ResourceCenterTabBar.js.map +1 -0
  42. package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js +122 -0
  43. package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js.map +1 -0
  44. package/lib/module/screens/ResourceCenter/ResourceEmptyState.js +36 -0
  45. package/lib/module/screens/ResourceCenter/ResourceEmptyState.js.map +1 -0
  46. package/lib/module/screens/ResourceCenter/index.js +104 -0
  47. package/lib/module/screens/ResourceCenter/index.js.map +1 -0
  48. package/lib/module/screens/ResourceCenter/types.js +25 -0
  49. package/lib/module/screens/ResourceCenter/types.js.map +1 -0
  50. package/lib/module/screens/chat-detail/BotMenuSheet.js +73 -0
  51. package/lib/module/screens/chat-detail/BotMenuSheet.js.map +1 -0
  52. package/lib/module/screens/chat-detail/ChatComposer.js +78 -3
  53. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  54. package/lib/module/screens/chat-detail/components/ChatMessageInput.js +11 -6
  55. package/lib/module/screens/chat-detail/components/ChatMessageInput.js.map +1 -1
  56. package/lib/module/services/attachmentOrchestrator.js +19 -4
  57. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  58. package/lib/module/services/endpoints.js +1 -0
  59. package/lib/module/services/endpoints.js.map +1 -1
  60. package/lib/module/services/resourceCenter.js +102 -0
  61. package/lib/module/services/resourceCenter.js.map +1 -0
  62. package/lib/module/translation/resources/i18n.js +13 -1
  63. package/lib/module/translation/resources/i18n.js.map +1 -1
  64. package/lib/module/types/attachment.js +2 -0
  65. package/lib/module/types/chat.js +20 -0
  66. package/lib/module/types/chat.js.map +1 -1
  67. package/lib/module/utils/url.js +11 -0
  68. package/lib/module/utils/url.js.map +1 -1
  69. package/lib/typescript/src/assets/images/index.d.ts +1 -0
  70. package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
  71. package/lib/typescript/src/components/MediaViewerModal.d.ts +3 -1
  72. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -1
  73. package/lib/typescript/src/components/MergedImageGrid.d.ts.map +1 -1
  74. package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
  75. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  76. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
  77. package/lib/typescript/src/hooks/flows/useFlow.d.ts +1 -0
  78. package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -1
  79. package/lib/typescript/src/hooks/query-keys.d.ts +5 -0
  80. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  81. package/lib/typescript/src/hooks/resourceCenter/useThreadResources.d.ts +21 -0
  82. package/lib/typescript/src/hooks/resourceCenter/useThreadResources.d.ts.map +1 -0
  83. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  84. package/lib/typescript/src/hooks/useMediaViewer.d.ts +5 -1
  85. package/lib/typescript/src/hooks/useMediaViewer.d.ts.map +1 -1
  86. package/lib/typescript/src/hooks/useSendAttachment.d.ts.map +1 -1
  87. package/lib/typescript/src/index.d.ts +2 -0
  88. package/lib/typescript/src/index.d.ts.map +1 -1
  89. package/lib/typescript/src/screens/MediaView/index.d.ts.map +1 -1
  90. package/lib/typescript/src/screens/ResourceCenter/FileResourceRow.d.ts +7 -0
  91. package/lib/typescript/src/screens/ResourceCenter/FileResourceRow.d.ts.map +1 -0
  92. package/lib/typescript/src/screens/ResourceCenter/GridRenderer.d.ts +14 -0
  93. package/lib/typescript/src/screens/ResourceCenter/GridRenderer.d.ts.map +1 -0
  94. package/lib/typescript/src/screens/ResourceCenter/GridThumbnail.d.ts +10 -0
  95. package/lib/typescript/src/screens/ResourceCenter/GridThumbnail.d.ts.map +1 -0
  96. package/lib/typescript/src/screens/ResourceCenter/LinkResourceRow.d.ts +8 -0
  97. package/lib/typescript/src/screens/ResourceCenter/LinkResourceRow.d.ts.map +1 -0
  98. package/lib/typescript/src/screens/ResourceCenter/ListRenderer.d.ts +16 -0
  99. package/lib/typescript/src/screens/ResourceCenter/ListRenderer.d.ts.map +1 -0
  100. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabBar.d.ts +3 -0
  101. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabBar.d.ts.map +1 -0
  102. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts +16 -0
  103. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts.map +1 -0
  104. package/lib/typescript/src/screens/ResourceCenter/ResourceEmptyState.d.ts +6 -0
  105. package/lib/typescript/src/screens/ResourceCenter/ResourceEmptyState.d.ts.map +1 -0
  106. package/lib/typescript/src/screens/ResourceCenter/index.d.ts +4 -0
  107. package/lib/typescript/src/screens/ResourceCenter/index.d.ts.map +1 -0
  108. package/lib/typescript/src/screens/ResourceCenter/types.d.ts +28 -0
  109. package/lib/typescript/src/screens/ResourceCenter/types.d.ts.map +1 -0
  110. package/lib/typescript/src/screens/chat-detail/BotMenuSheet.d.ts +7 -0
  111. package/lib/typescript/src/screens/chat-detail/BotMenuSheet.d.ts.map +1 -0
  112. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  113. package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts +2 -1
  114. package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts.map +1 -1
  115. package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
  116. package/lib/typescript/src/services/endpoints.d.ts +1 -0
  117. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  118. package/lib/typescript/src/services/resourceCenter.d.ts +9 -0
  119. package/lib/typescript/src/services/resourceCenter.d.ts.map +1 -0
  120. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  121. package/lib/typescript/src/types/attachment.d.ts +2 -0
  122. package/lib/typescript/src/types/attachment.d.ts.map +1 -1
  123. package/lib/typescript/src/types/chat.d.ts +60 -1
  124. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  125. package/lib/typescript/src/utils/url.d.ts +7 -0
  126. package/lib/typescript/src/utils/url.d.ts.map +1 -1
  127. package/package.json +1 -1
  128. package/src/assets/images/index.ts +1 -0
  129. package/src/assets/images/resource-center-empty-box.png +0 -0
  130. package/src/components/MediaViewerModal.tsx +8 -3
  131. package/src/components/MergedImageGrid.tsx +1 -0
  132. package/src/config/api-endpoints.ts +2 -0
  133. package/src/context/ChatDetailContext.tsx +1 -0
  134. package/src/core/notification/pushToken.ts +7 -7
  135. package/src/hooks/flows/useFlow.ts +14 -0
  136. package/src/hooks/query-keys.ts +10 -0
  137. package/src/hooks/resourceCenter/useThreadResources.ts +124 -0
  138. package/src/hooks/useImageAttachment.ts +1 -2
  139. package/src/hooks/useMediaViewer.ts +21 -7
  140. package/src/hooks/useSendAttachment.ts +4 -1
  141. package/src/index.tsx +2 -0
  142. package/src/screens/MediaView/index.tsx +117 -54
  143. package/src/screens/ResourceCenter/FileResourceRow.tsx +99 -0
  144. package/src/screens/ResourceCenter/GridRenderer.tsx +104 -0
  145. package/src/screens/ResourceCenter/GridThumbnail.tsx +98 -0
  146. package/src/screens/ResourceCenter/LinkResourceRow.tsx +96 -0
  147. package/src/screens/ResourceCenter/ListRenderer.tsx +81 -0
  148. package/src/screens/ResourceCenter/ResourceCenterTabBar.tsx +89 -0
  149. package/src/screens/ResourceCenter/ResourceCenterTabScene.tsx +142 -0
  150. package/src/screens/ResourceCenter/ResourceEmptyState.tsx +39 -0
  151. package/src/screens/ResourceCenter/index.tsx +129 -0
  152. package/src/screens/ResourceCenter/types.ts +56 -0
  153. package/src/screens/chat-detail/BotMenuSheet.tsx +94 -0
  154. package/src/screens/chat-detail/ChatComposer.tsx +100 -3
  155. package/src/screens/chat-detail/components/ChatMessageInput.tsx +11 -7
  156. package/src/services/attachmentOrchestrator.ts +15 -2
  157. package/src/services/endpoints.ts +2 -0
  158. package/src/services/resourceCenter.ts +137 -0
  159. package/src/translation/resources/i18n.ts +13 -0
  160. package/src/types/attachment.ts +3 -0
  161. package/src/types/chat.ts +75 -0
  162. package/src/utils/url.ts +11 -0
@@ -0,0 +1,142 @@
1
+ import { memo, useCallback } from 'react';
2
+ import { ActivityIndicator, StyleSheet } from 'react-native';
3
+ import type { RefObject } from 'react';
4
+ import { KContainer, KLabel, KColors } from '@droppii/libs';
5
+ import { trans } from '../../translation';
6
+ import { useThreadResources } from '../../hooks/resourceCenter/useThreadResources';
7
+ import { ResourceEmptyState } from './ResourceEmptyState';
8
+ import { GridRenderer } from './GridRenderer';
9
+ import { ListRenderer } from './ListRenderer';
10
+ import { DThreadResourceContentType } from '../../types/chat';
11
+ import type { ResourceCenterLayout } from './types';
12
+ import type { MediaViewerModalHandle } from '../../components/MediaViewerModal';
13
+
14
+ interface Props {
15
+ recvID: string;
16
+ chatType: 'SINGLE' | 'GROUP';
17
+ contentType: DThreadResourceContentType;
18
+ layout: ResourceCenterLayout;
19
+ emptyKey: string;
20
+ onPressUrl?: (url: string) => void;
21
+ mediaViewerRef: RefObject<MediaViewerModalHandle | null>;
22
+ }
23
+
24
+ // One scene per tab — owns its own `useThreadResources` call so react-query
25
+ // caches independently per (recvID, contentType) and `lazy` TabView scenes
26
+ // don't all fetch on mount.
27
+ //
28
+ // Known limitation (deliberate, not an oversight): the shared `MediaViewerModal`
29
+ // is opened with a static snapshot of the currently-loaded page. If the user swipes
30
+ // to the last loaded item while more pages exist server-side, the viewer does not
31
+ // auto-fetch more — they need to close it, scroll the grid to trigger `loadMore`,
32
+ // then reopen. Wiring live pagination into the viewer would require either coupling
33
+ // the shared `MediaView` (used by in-chat bubbles too) to react-query state, or
34
+ // forking its ~230 lines of zoom/video/download logic into a separate component —
35
+ // not worth it for this ticket's scope.
36
+ export const ResourceCenterTabScene = memo(
37
+ ({
38
+ recvID,
39
+ chatType,
40
+ contentType,
41
+ layout,
42
+ emptyKey,
43
+ onPressUrl,
44
+ mediaViewerRef,
45
+ }: Props) => {
46
+ const {
47
+ items,
48
+ isLoading,
49
+ isError,
50
+ loadMore,
51
+ isFetchingNextPage,
52
+ isRefetching,
53
+ refetch,
54
+ refreshFirstPage,
55
+ } = useThreadResources(recvID, contentType, chatType);
56
+
57
+ const onPressGridItem = useCallback(
58
+ (index: number) => {
59
+ const mediaItems = items.map((item) => ({
60
+ url: item.sourceUrl ?? '',
61
+ isVideo: contentType === DThreadResourceContentType.VIDEO,
62
+ }));
63
+ // Resource Center grids (Ảnh/Video tabs) can hold hundreds of items —
64
+ // the viewer's dot/counter pagination indicator is meant for small,
65
+ // in-chat message groups and becomes noise at this scale. Hide it here.
66
+ mediaViewerRef.current?.show(mediaItems, index, { pagination: false });
67
+ },
68
+ [items, contentType, mediaViewerRef]
69
+ );
70
+
71
+ if (isLoading) {
72
+ return (
73
+ <KContainer.View style={styles.centerFill}>
74
+ <ActivityIndicator color={KColors.palette.primary.w400} />
75
+ </KContainer.View>
76
+ );
77
+ }
78
+
79
+ if (isError) {
80
+ return (
81
+ <KContainer.View style={styles.centerFill}>
82
+ <KLabel.Text
83
+ typo="TextSmNormal"
84
+ color={KColors.palette.gray.w400}
85
+ style={styles.errorText}
86
+ >
87
+ {trans('chat:generic_error')}
88
+ </KLabel.Text>
89
+ <KContainer.Touchable onPress={() => refetch()}>
90
+ <KLabel.Text typo="TextNmBold" color={KColors.palette.primary.w400}>
91
+ {trans('chat:retry')}
92
+ </KLabel.Text>
93
+ </KContainer.Touchable>
94
+ </KContainer.View>
95
+ );
96
+ }
97
+
98
+ // `ResourceEmptyState` is passed as `ListEmptyComponent` (not an early return)
99
+ // so the list itself — and its `RefreshControl` — stays mounted even with 0
100
+ // items, letting pull-to-refresh work on the empty state too.
101
+ const emptyComponent = <ResourceEmptyState emptyKey={emptyKey} />;
102
+
103
+ if (layout === 'grid') {
104
+ return (
105
+ <GridRenderer
106
+ items={items}
107
+ onEndReached={loadMore}
108
+ isFetchingNextPage={isFetchingNextPage}
109
+ onPressItem={onPressGridItem}
110
+ onRefresh={refreshFirstPage}
111
+ isRefetching={isRefetching}
112
+ emptyComponent={emptyComponent}
113
+ />
114
+ );
115
+ }
116
+
117
+ return (
118
+ <ListRenderer
119
+ items={items}
120
+ contentType={contentType}
121
+ onEndReached={loadMore}
122
+ isFetchingNextPage={isFetchingNextPage}
123
+ onPressUrl={onPressUrl}
124
+ onRefresh={refreshFirstPage}
125
+ isRefetching={isRefetching}
126
+ emptyComponent={emptyComponent}
127
+ />
128
+ );
129
+ }
130
+ );
131
+
132
+ const styles = StyleSheet.create({
133
+ centerFill: {
134
+ flex: 1,
135
+ alignItems: 'center',
136
+ justifyContent: 'center',
137
+ gap: 8,
138
+ },
139
+ errorText: {
140
+ textAlign: 'center',
141
+ },
142
+ });
@@ -0,0 +1,39 @@
1
+ import { memo } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { KContainer, KImage, KLabel, KColors } from '@droppii/libs';
4
+ import { trans } from '../../translation';
5
+ import Images from '../../assets/images';
6
+
7
+ interface Props {
8
+ emptyKey: string;
9
+ }
10
+
11
+ export const ResourceEmptyState = memo(({ emptyKey }: Props) => (
12
+ <KContainer.View style={styles.wrapper}>
13
+ <KImage.Base
14
+ uri={Images.RESOURCE_CENTER_EMPTY_BOX}
15
+ width={120}
16
+ height={120}
17
+ />
18
+ <KLabel.Text
19
+ typo="TextSmNormal"
20
+ color={KColors.palette.gray.w400}
21
+ style={styles.text}
22
+ >
23
+ {trans(emptyKey)}
24
+ </KLabel.Text>
25
+ </KContainer.View>
26
+ ));
27
+
28
+ const styles = StyleSheet.create({
29
+ wrapper: {
30
+ flex: 1,
31
+ alignItems: 'center',
32
+ justifyContent: 'center',
33
+ paddingTop: 60,
34
+ gap: 12,
35
+ },
36
+ text: {
37
+ textAlign: 'center',
38
+ },
39
+ });
@@ -0,0 +1,129 @@
1
+ import { memo, useCallback, useMemo, useRef, useState } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { TabView } from 'react-native-tab-view';
4
+ import { KNavigation, KContainer, KColors } from '@droppii/libs';
5
+ import { SessionType } from '@droppii/openim-rn-client-sdk';
6
+ import { trans } from '../../translation';
7
+ import { useConversation } from '../../store';
8
+ import {
9
+ MediaViewerModal,
10
+ type MediaViewerModalHandle,
11
+ } from '../../components/MediaViewerModal';
12
+ import { ResourceCenterTabBar } from './ResourceCenterTabBar';
13
+ import { ResourceCenterTabScene } from './ResourceCenterTabScene';
14
+ import { RESOURCE_TABS } from './types';
15
+ import type { ResourceCenterScreenProps, ResourceCenterLayout } from './types';
16
+ import type { DThreadResourceContentType } from '../../types/chat';
17
+
18
+ interface TabRoute {
19
+ key: string;
20
+ title: string;
21
+ contentType: DThreadResourceContentType;
22
+ layout: ResourceCenterLayout;
23
+ emptyKey: string;
24
+ }
25
+
26
+ const ResourceCenterScreen = memo(
27
+ ({
28
+ conversationId,
29
+ fallbackGroupID,
30
+ fallbackUserID,
31
+ initialTab,
32
+ onBack,
33
+ onPressUrl,
34
+ }: ResourceCenterScreenProps) => {
35
+ // Same derivation every host app previously duplicated: groupID-first
36
+ // (group/bot threads), userID fallback (1-1 threads), with route-param
37
+ // fallbacks for the window before `conversationId` lands in the store.
38
+ const conversation = useConversation(conversationId);
39
+ const groupID = conversation?.groupID || fallbackGroupID;
40
+ const recvID = groupID || conversation?.userID || fallbackUserID || '';
41
+ // `chatType` must reflect the conversation's actual session type, not just
42
+ // "did a groupID show up somewhere" — `conversationType` (SessionType.Group
43
+ // === 3, covers WorkingGroup too since it shares the same numeric value) is
44
+ // the authoritative signal once the conversation is loaded. Only fall back
45
+ // to the groupID-presence heuristic for the window before it lands in the
46
+ // store (conversation === undefined).
47
+ const isGroupConversation = conversation
48
+ ? conversation?.conversationType === SessionType.Group
49
+ : !!groupID;
50
+ const chatType: 'SINGLE' | 'GROUP' = isGroupConversation
51
+ ? 'GROUP'
52
+ : 'SINGLE';
53
+
54
+ const [routes] = useState<TabRoute[]>(() =>
55
+ RESOURCE_TABS.map((tab) => ({
56
+ key: String(tab.contentType),
57
+ title: trans(tab.labelKey),
58
+ contentType: tab.contentType,
59
+ layout: tab.layout,
60
+ emptyKey: tab.emptyKey,
61
+ }))
62
+ );
63
+
64
+ const initialIndex = useMemo(() => {
65
+ const idx = routes.findIndex((r) => r.contentType === initialTab);
66
+ return idx >= 0 ? idx : 0;
67
+ }, [routes, initialTab]);
68
+
69
+ const [index, setIndex] = useState(initialIndex);
70
+ const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
71
+
72
+ // `renderScene` is a render prop consumed by `TabView` — matching `Inbox.tsx`'s
73
+ // convention of `useCallback`-wrapping it so it doesn't get a new identity (and
74
+ // force unnecessary internal re-renders) on every parent render.
75
+ // `renderTabBar` needs no wrapper: `ResourceCenterTabBar` already implements
76
+ // `TabView`'s own `renderTabBar` prop contract directly (it wraps the library's
77
+ // `TabBar`), so its component reference — already stable — is passed as-is.
78
+ const renderScene = useCallback(
79
+ ({ route }: { route: TabRoute }) => (
80
+ <ResourceCenterTabScene
81
+ recvID={recvID}
82
+ chatType={chatType}
83
+ contentType={route.contentType}
84
+ layout={route.layout}
85
+ emptyKey={route.emptyKey}
86
+ onPressUrl={onPressUrl}
87
+ mediaViewerRef={mediaViewerRef}
88
+ />
89
+ ),
90
+ [recvID, chatType, onPressUrl]
91
+ );
92
+
93
+ return (
94
+ <KContainer.View style={styles.flex}>
95
+ <KNavigation.Header
96
+ title={trans('chat:resource_center_title')}
97
+ alignment="left"
98
+ size="small"
99
+ theme="light"
100
+ leftButton={{
101
+ id: 'back',
102
+ icon: { vectorName: 'arrow-left-o' },
103
+ onPress: onBack,
104
+ }}
105
+ />
106
+ <TabView<TabRoute>
107
+ navigationState={{ index, routes }}
108
+ onIndexChange={setIndex}
109
+ lazy
110
+ renderTabBar={ResourceCenterTabBar}
111
+ renderScene={renderScene}
112
+ />
113
+
114
+ <MediaViewerModal ref={mediaViewerRef} />
115
+ </KContainer.View>
116
+ );
117
+ }
118
+ );
119
+
120
+ ResourceCenterScreen.displayName = 'Screen.ResourceCenter';
121
+
122
+ export default ResourceCenterScreen;
123
+
124
+ const styles = StyleSheet.create({
125
+ flex: {
126
+ flex: 1,
127
+ backgroundColor: KColors.white,
128
+ },
129
+ });
@@ -0,0 +1,56 @@
1
+ import { DThreadResourceContentType } from '../../types/chat';
2
+
3
+ export interface ResourceCenterScreenProps {
4
+ conversationId: string;
5
+ /**
6
+ * Fallback groupID/userID for the race-condition window where `conversationId`
7
+ * isn't in the store yet (fresh deep-link / cold nav). Mirrors the route-param
8
+ * fallback pattern host apps already use elsewhere (e.g. `useSendMessage`).
9
+ */
10
+ fallbackGroupID?: string;
11
+ fallbackUserID?: string;
12
+ initialTab?: DThreadResourceContentType;
13
+ onBack?: () => void;
14
+ /**
15
+ * Host-provided link-open handler — mirrors `ChatDetailProps.onPressUrl`. This library does
16
+ * NOT implement in-app-browser/deeplink/external-app logic itself (traced: `ChatDetail.tsx`
17
+ * only normalizes the URL and forwards to this same prop) — the host app supplies it.
18
+ */
19
+ onPressUrl?: (url: string) => void;
20
+ }
21
+
22
+ export type ResourceCenterLayout = 'grid' | 'list';
23
+
24
+ export interface ResourceTabConfig {
25
+ contentType: DThreadResourceContentType;
26
+ labelKey: string;
27
+ emptyKey: string;
28
+ layout: ResourceCenterLayout;
29
+ }
30
+
31
+ export const RESOURCE_TABS: ResourceTabConfig[] = [
32
+ {
33
+ contentType: DThreadResourceContentType.IMAGE,
34
+ labelKey: 'chat:resource_center_tab_image',
35
+ emptyKey: 'chat:resource_center_empty_generic',
36
+ layout: 'grid',
37
+ },
38
+ {
39
+ contentType: DThreadResourceContentType.VIDEO,
40
+ labelKey: 'chat:resource_center_tab_video',
41
+ emptyKey: 'chat:resource_center_empty_generic',
42
+ layout: 'grid',
43
+ },
44
+ {
45
+ contentType: DThreadResourceContentType.FILE,
46
+ labelKey: 'chat:resource_center_tab_file',
47
+ emptyKey: 'chat:resource_center_empty_generic',
48
+ layout: 'list',
49
+ },
50
+ {
51
+ contentType: DThreadResourceContentType.URL_TEXT,
52
+ labelKey: 'chat:resource_center_tab_link',
53
+ emptyKey: 'chat:resource_center_empty_link',
54
+ layout: 'list',
55
+ },
56
+ ];
@@ -0,0 +1,94 @@
1
+ import { memo, useCallback } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import {
4
+ KContainer,
5
+ KLabel,
6
+ KColors,
7
+ KImage,
8
+ KSpacingValue,
9
+ KDivider,
10
+ } from '@droppii/libs';
11
+ import { UIUtils } from '../../utils/UIUtils';
12
+ import { trans } from '../../translation';
13
+ import type { InputButtonItem } from '../../types/flows';
14
+ import { DEFAULT_BUTTON_ID } from '../../utils/flows';
15
+
16
+ interface BotMenuSheetContentProps {
17
+ options: InputButtonItem[];
18
+ onSelect: (item: InputButtonItem) => void;
19
+ }
20
+
21
+ const BotMenuSheetContent = memo(
22
+ ({ options, onSelect }: BotMenuSheetContentProps) => {
23
+ const handlePress = useCallback(
24
+ (item: InputButtonItem) => {
25
+ UIUtils.bottomSheet.dismiss();
26
+ onSelect(item);
27
+ },
28
+ [onSelect]
29
+ );
30
+
31
+ return (
32
+ <KContainer.View paddingH="0.75rem" paddingV="0.25rem">
33
+ {options?.map((item, index) => (
34
+ <>
35
+ <KContainer.Touchable
36
+ key={item.id}
37
+ row
38
+ alignItems
39
+ justifyContent="space-between"
40
+ style={styles.item}
41
+ onPress={() => handlePress(item)}
42
+ >
43
+ <KLabel.Text typo="TextMdMedium" flex>
44
+ {item?.id === DEFAULT_BUTTON_ID
45
+ ? trans('restart')
46
+ : item?.label || ''}
47
+ </KLabel.Text>
48
+ <KImage.VectorIcons
49
+ name="angle-right-o"
50
+ size={20}
51
+ color={KColors.gray.light}
52
+ />
53
+ </KContainer.Touchable>
54
+ <KContainer.VisibleView visible={index !== options?.length - 1}>
55
+ <KDivider size="hairline" />
56
+ </KContainer.VisibleView>
57
+ </>
58
+ ))}
59
+ </KContainer.View>
60
+ );
61
+ }
62
+ );
63
+
64
+ BotMenuSheetContent.displayName = 'BotMenuSheetContent';
65
+
66
+ const styles = StyleSheet.create({
67
+ item: {
68
+ paddingVertical: KSpacingValue['0.75rem'],
69
+ },
70
+ });
71
+
72
+ export interface OpenBotMenuSheetParams {
73
+ options: InputButtonItem[];
74
+ onSelect: (item: InputButtonItem) => void;
75
+ }
76
+
77
+ export const openBotMenuSheet = ({
78
+ options,
79
+ onSelect,
80
+ }: OpenBotMenuSheetParams) => {
81
+ UIUtils.bottomSheet.open({
82
+ header: {
83
+ title: { text: trans('chat:bot_menu_title') },
84
+ alignment: 'left',
85
+ showCloseButton: true,
86
+ },
87
+ touchOutsideToDismiss: true,
88
+ body: {
89
+ renderContent: () => (
90
+ <BotMenuSheetContent options={options} onSelect={onSelect} />
91
+ ),
92
+ },
93
+ });
94
+ };
@@ -48,13 +48,17 @@ import { useChatComposerState } from './hooks/useChatComposerState';
48
48
  import { useChatComposerAnimation } from './hooks/useChatComposerAnimation';
49
49
  import { useChatDetailContext } from '../../context/ChatDetailContext';
50
50
  import { trans } from '../../translation';
51
- import { useRunFlow } from '../../hooks/flows/useFlow';
52
- import { ReactFlowsNodeTypes } from '../../types/flows';
51
+ import { useCloseSupportSession, useRunFlow } from '../../hooks/flows/useFlow';
52
+ import { ReactFlowsNodeTypes, type InputButtonItem } from '../../types/flows';
53
+ import { isContextButton } from '../../utils/flows';
54
+ import { DFlowState } from '../../types/chat';
53
55
  import { useAttachmentMapping } from './hooks/useAttachmentMapping';
54
56
  import type { AttachmentType } from '../../types/attachment';
55
57
  import { useChatCompose } from '../../hooks/useChatCompose';
56
58
  import { ChatCurrentQuotedMessage } from './ChatCurrentQuotedMessage';
57
59
  import { useSendMessage } from '../../hooks/message/useSendMessage';
60
+ import { openBotMenuSheet } from './BotMenuSheet';
61
+ import { UIUtils } from '../../utils/UIUtils';
58
62
 
59
63
  interface ChatComposerInternalProps {
60
64
  isEmptyMessage?: boolean;
@@ -76,8 +80,14 @@ export const ChatComposer = memo(
76
80
  const isDisableCompose = isComposeDisabled;
77
81
  const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
78
82
 
79
- const { isBotConversation, conversationFlow } = useChatDetailContext();
83
+ const {
84
+ isBotConversation,
85
+ conversationFlow,
86
+ conversationFlowMainMenu,
87
+ flowState,
88
+ } = useChatDetailContext();
80
89
  const { mutate: runFlow, isPending } = useRunFlow();
90
+ const { mutateAsync: closeSupportSession } = useCloseSupportSession();
81
91
  const { sendMessage } = useSendMessage();
82
92
  const compose = useChatCompose();
83
93
 
@@ -205,6 +215,92 @@ export const ChatComposer = memo(
205
215
  });
206
216
  }, [runFlow, conversationFlow]);
207
217
 
218
+ const botMenuOptions = useMemo(
219
+ () =>
220
+ conversationFlowMainMenu?.filter(
221
+ (item) => !isContextButton(item.label)
222
+ ) || [],
223
+ [conversationFlowMainMenu]
224
+ );
225
+
226
+ const hasBotMenuOptions = isBotConversation && botMenuOptions.length > 0;
227
+
228
+ const runBotMenuOption = useCallback(
229
+ (item: InputButtonItem) => {
230
+ runFlow({
231
+ actionId: item.id,
232
+ groupId: '',
233
+ childId: '',
234
+ plainText: item.label,
235
+ });
236
+ },
237
+ [runFlow]
238
+ );
239
+
240
+ const handleSelectBotMenuOption = useCallback(
241
+ (item: InputButtonItem) => {
242
+ if (flowState === DFlowState.CS) {
243
+ UIUtils.popup.open({
244
+ header: {
245
+ title: { text: trans('chat:end_support_session_title') },
246
+ alignment: 'center',
247
+ },
248
+ body: {
249
+ renderContent: () => (
250
+ <KContainer.View padding={'1rem'}>
251
+ <KLabel.RichText
252
+ typo="TextMdNormal"
253
+ textAlign="center"
254
+ color={KColors.gray.dark}
255
+ richTextOptions={{
256
+ i18nKey: trans('chat:end_support_session_desc'),
257
+ components: {
258
+ bold: (
259
+ <KLabel.Text
260
+ typo="TextMdBold"
261
+ color={KColors.gray.dark}
262
+ />
263
+ ),
264
+ },
265
+ }}
266
+ />
267
+ </KContainer.View>
268
+ ),
269
+ },
270
+ actions: {
271
+ buttons: [
272
+ {
273
+ label: trans('chat:end_support_session_secondary'),
274
+ kind: 'light',
275
+ onPress: () => {},
276
+ },
277
+ {
278
+ label: trans('chat:end_support_session_primary'),
279
+ kind: 'danger',
280
+ onPress: async () => {
281
+ await closeSupportSession();
282
+ runBotMenuOption(item);
283
+ },
284
+ },
285
+ ],
286
+ type: 'horizontal-button',
287
+ },
288
+ });
289
+ return;
290
+ }
291
+ runBotMenuOption(item);
292
+ },
293
+ [flowState, runBotMenuOption, closeSupportSession]
294
+ );
295
+
296
+ const handleOpenBotMenu = useCallback(() => {
297
+ if (!hasBotMenuOptions) return;
298
+ openBotMenuSheet({
299
+ options: botMenuOptions,
300
+ onSelect: handleSelectBotMenuOption,
301
+ });
302
+ }, [hasBotMenuOptions, botMenuOptions, handleSelectBotMenuOption]);
303
+
208
304
  useEffect(() => {
209
305
  if (!compose.text) setIsInputWrapped(false);
210
306
  }, [compose.text, setIsInputWrapped]);
@@ -373,6 +469,7 @@ export const ChatComposer = memo(
373
469
  isMultilineInput={isMultilineInput}
374
470
  inputRef={inputRef}
375
471
  onEmojiPress={handleToggleSticker}
472
+ onSuggestPress={hasBotMenuOptions ? handleOpenBotMenu : undefined}
376
473
  />
377
474
 
378
475
  <KContainer.Touchable
@@ -25,6 +25,7 @@ export interface ChatMessageInputProps {
25
25
  isInputFocused: boolean;
26
26
  isMultilineInput: boolean;
27
27
  onEmojiPress?: () => void;
28
+ onSuggestPress?: () => void;
28
29
  inputRef: React.RefObject<TextInput>;
29
30
  }
30
31
 
@@ -39,6 +40,7 @@ export const ChatMessageInput = memo(
39
40
  isInputFocused,
40
41
  isMultilineInput,
41
42
  onEmojiPress,
43
+ onSuggestPress,
42
44
  inputRef,
43
45
  }: ChatMessageInputProps) => {
44
46
  const renderEmojiNode = useCallback(
@@ -69,13 +71,15 @@ export const ChatMessageInput = memo(
69
71
  paddingL="0.75rem"
70
72
  style={styles.inputContainer}
71
73
  >
72
- <KContainer.View marginR="0.75rem">
73
- <KImage.VectorIcons
74
- name="chat-suggest-o"
75
- size={24}
76
- color={KColors.gray.normal}
77
- />
78
- </KContainer.View>
74
+ <KContainer.VisibleView visible={!!onSuggestPress}>
75
+ <KContainer.Touchable marginR="0.75rem" onPress={onSuggestPress}>
76
+ <KImage.VectorIcons
77
+ name="chat-suggest-o"
78
+ size={24}
79
+ color={KColors.gray.normal}
80
+ />
81
+ </KContainer.Touchable>
82
+ </KContainer.VisibleView>
79
83
 
80
84
  <TextInput
81
85
  ref={inputRef}
@@ -11,6 +11,7 @@ import type {
11
11
  AttachmentSendResult,
12
12
  } from '../types/attachment';
13
13
  import { getAttachmentHandler } from '../types/attachmentHandler';
14
+ import type { DChatApplicationType, IMessageItemEx } from '../types/chat';
14
15
  import { useConversationStore } from '../store';
15
16
  import { useMessageStore } from '../store/message';
16
17
  import {
@@ -97,6 +98,7 @@ export namespace AttachmentOrchestratorService {
97
98
  try {
98
99
  const mergedMsgId = await sendMergedMessage(
99
100
  payloads.map((p) => p.payload),
101
+ options.applicationType,
100
102
  options.includeCaption
101
103
  );
102
104
  console.log('[AttachmentOrchestrator] Messages sent via merger:', {
@@ -122,6 +124,7 @@ export namespace AttachmentOrchestratorService {
122
124
 
123
125
  async function sendMergedMessage(
124
126
  payloads: unknown[],
127
+ applicationType?: DChatApplicationType,
125
128
  _caption?: string
126
129
  ): Promise<string> {
127
130
  const currentConversation = useConversationStore
@@ -274,8 +277,18 @@ export namespace AttachmentOrchestratorService {
274
277
  }
275
278
  );
276
279
 
277
- const recvID = currentConversation.userID || '';
278
- const groupID = currentConversation.groupID || '';
280
+ const { currentUserId, currentGroupId } = useConversationStore.getState();
281
+ const recvID = currentConversation.userID || currentUserId || '';
282
+ const groupID = currentConversation.groupID || currentGroupId || '';
283
+
284
+ // Attachment messages otherwise skip `ex`, unlike text messages
285
+ // (buildSendParams), leaving applicationType context missing.
286
+ if (applicationType) {
287
+ const extendMessageInfo: IMessageItemEx = { applicationType };
288
+ for (let i = 0; i < allMessages.length; i++) {
289
+ (allMessages[i] as MessageItem).ex = JSON.stringify(extendMessageInfo);
290
+ }
291
+ }
279
292
 
280
293
  // Push all messages as "Sending" immediately so the user sees every
281
294
  // bubble right away, instead of waiting for each network call in turn.
@@ -28,6 +28,8 @@ export const ENDPOINTS = {
28
28
  `${getApiEndpoint('bot')}/${botId}/conversation`,
29
29
  conversationFlowMainMenu: (id: string) =>
30
30
  `${getApiEndpoint('conversation')}/${id}/bot-flows/main-menu`,
31
+ closeConversationSession: (id: string) =>
32
+ `${getApiEndpoint('conversation')}/${id}/session/close`,
31
33
  getConversationStatusByBotId: (botId: string) =>
32
34
  `${getApiEndpoint('bot')}/${botId}`,
33
35
  getConversationTarget: (conversationId: string) =>