@droppii-org/chat-mobile 0.2.50 → 0.2.52

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 (248) 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/components/StickerPicker/StickerCategoryTabs.js +86 -0
  9. package/lib/module/components/StickerPicker/StickerCategoryTabs.js.map +1 -0
  10. package/lib/module/components/StickerPicker/StickerPickerPlaceholder.js +18 -0
  11. package/lib/module/components/StickerPicker/StickerPickerPlaceholder.js.map +1 -0
  12. package/lib/module/components/StickerPicker/StickerPickerRowItem.js +35 -0
  13. package/lib/module/components/StickerPicker/StickerPickerRowItem.js.map +1 -0
  14. package/lib/module/components/StickerPicker/StickerSectionList.js +110 -0
  15. package/lib/module/components/StickerPicker/StickerSectionList.js.map +1 -0
  16. package/lib/module/components/StickerPicker/constants.js +14 -0
  17. package/lib/module/components/StickerPicker/constants.js.map +1 -0
  18. package/lib/module/components/StickerPicker/index.js +134 -0
  19. package/lib/module/components/StickerPicker/index.js.map +1 -0
  20. package/lib/module/components/messages/stickerMessage/index.js +28 -0
  21. package/lib/module/components/messages/stickerMessage/index.js.map +1 -0
  22. package/lib/module/config/api-endpoints.js +5 -1
  23. package/lib/module/config/api-endpoints.js.map +1 -1
  24. package/lib/module/core/notification/pushToken.js +7 -7
  25. package/lib/module/core/notification/pushToken.js.map +1 -1
  26. package/lib/module/hooks/conversation/useGetConversationDetail.js +15 -0
  27. package/lib/module/hooks/conversation/useGetConversationDetail.js.map +1 -1
  28. package/lib/module/hooks/flows/useGetConversationFlowMainMenu.js +1 -1
  29. package/lib/module/hooks/flows/useGetConversationFlowMainMenu.js.map +1 -1
  30. package/lib/module/hooks/message/useSendMessage.js +45 -18
  31. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  32. package/lib/module/hooks/query-keys.js +11 -0
  33. package/lib/module/hooks/query-keys.js.map +1 -1
  34. package/lib/module/hooks/resourceCenter/useThreadResources.js +116 -0
  35. package/lib/module/hooks/resourceCenter/useThreadResources.js.map +1 -0
  36. package/lib/module/hooks/stickers/useFetchStickerByCategory.js +12 -0
  37. package/lib/module/hooks/stickers/useFetchStickerByCategory.js.map +1 -0
  38. package/lib/module/hooks/stickers/useStickerCategories.js +14 -0
  39. package/lib/module/hooks/stickers/useStickerCategories.js.map +1 -0
  40. package/lib/module/hooks/stickers/useStickerHistory.js +16 -0
  41. package/lib/module/hooks/stickers/useStickerHistory.js.map +1 -0
  42. package/lib/module/hooks/useChatCompose.js +1 -18
  43. package/lib/module/hooks/useChatCompose.js.map +1 -1
  44. package/lib/module/hooks/useImageAttachment.js +0 -2
  45. package/lib/module/hooks/useImageAttachment.js.map +1 -1
  46. package/lib/module/hooks/useMediaViewer.js +4 -1
  47. package/lib/module/hooks/useMediaViewer.js.map +1 -1
  48. package/lib/module/hooks/useSendAttachment.js +4 -1
  49. package/lib/module/hooks/useSendAttachment.js.map +1 -1
  50. package/lib/module/index.js +1 -0
  51. package/lib/module/index.js.map +1 -1
  52. package/lib/module/screens/MediaView/index.js +104 -42
  53. package/lib/module/screens/MediaView/index.js.map +1 -1
  54. package/lib/module/screens/ResourceCenter/FileResourceRow.js +80 -0
  55. package/lib/module/screens/ResourceCenter/FileResourceRow.js.map +1 -0
  56. package/lib/module/screens/ResourceCenter/GridRenderer.js +83 -0
  57. package/lib/module/screens/ResourceCenter/GridRenderer.js.map +1 -0
  58. package/lib/module/screens/ResourceCenter/GridThumbnail.js +97 -0
  59. package/lib/module/screens/ResourceCenter/GridThumbnail.js.map +1 -0
  60. package/lib/module/screens/ResourceCenter/LinkResourceRow.js +81 -0
  61. package/lib/module/screens/ResourceCenter/LinkResourceRow.js.map +1 -0
  62. package/lib/module/screens/ResourceCenter/ListRenderer.js +60 -0
  63. package/lib/module/screens/ResourceCenter/ListRenderer.js.map +1 -0
  64. package/lib/module/screens/ResourceCenter/ResourceCenterTabBar.js +75 -0
  65. package/lib/module/screens/ResourceCenter/ResourceCenterTabBar.js.map +1 -0
  66. package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js +122 -0
  67. package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js.map +1 -0
  68. package/lib/module/screens/ResourceCenter/ResourceEmptyState.js +36 -0
  69. package/lib/module/screens/ResourceCenter/ResourceEmptyState.js.map +1 -0
  70. package/lib/module/screens/ResourceCenter/index.js +104 -0
  71. package/lib/module/screens/ResourceCenter/index.js.map +1 -0
  72. package/lib/module/screens/ResourceCenter/types.js +25 -0
  73. package/lib/module/screens/ResourceCenter/types.js.map +1 -0
  74. package/lib/module/screens/chat-detail/ChatComposer.js +64 -16
  75. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  76. package/lib/module/screens/chat-detail/ChatDetail.js +6 -20
  77. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  78. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +15 -4
  79. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
  80. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +3 -0
  81. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
  82. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +5 -6
  83. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  84. package/lib/module/services/attachmentOrchestrator.js +20 -5
  85. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  86. package/lib/module/services/endpoints.js +4 -1
  87. package/lib/module/services/endpoints.js.map +1 -1
  88. package/lib/module/services/resourceCenter.js +102 -0
  89. package/lib/module/services/resourceCenter.js.map +1 -0
  90. package/lib/module/store/conversation.js +1 -20
  91. package/lib/module/store/conversation.js.map +1 -1
  92. package/lib/module/translation/resources/i18n.js +10 -1
  93. package/lib/module/translation/resources/i18n.js.map +1 -1
  94. package/lib/module/types/attachment.js +2 -0
  95. package/lib/module/types/chat.js +26 -0
  96. package/lib/module/types/chat.js.map +1 -1
  97. package/lib/module/types/sticker.js +2 -0
  98. package/lib/module/types/sticker.js.map +1 -0
  99. package/lib/module/utils/conversation.js +1 -24
  100. package/lib/module/utils/conversation.js.map +1 -1
  101. package/lib/module/utils/messageUtils.js +2 -0
  102. package/lib/module/utils/messageUtils.js.map +1 -1
  103. package/lib/module/utils/url.js +11 -0
  104. package/lib/module/utils/url.js.map +1 -1
  105. package/lib/typescript/src/assets/images/index.d.ts +1 -0
  106. package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/MediaViewerModal.d.ts +3 -1
  108. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -1
  109. package/lib/typescript/src/components/MergedImageGrid.d.ts.map +1 -1
  110. package/lib/typescript/src/components/StickerPicker/StickerCategoryTabs.d.ts +9 -0
  111. package/lib/typescript/src/components/StickerPicker/StickerCategoryTabs.d.ts.map +1 -0
  112. package/lib/typescript/src/components/StickerPicker/StickerPickerPlaceholder.d.ts +2 -0
  113. package/lib/typescript/src/components/StickerPicker/StickerPickerPlaceholder.d.ts.map +1 -0
  114. package/lib/typescript/src/components/StickerPicker/StickerPickerRowItem.d.ts +8 -0
  115. package/lib/typescript/src/components/StickerPicker/StickerPickerRowItem.d.ts.map +1 -0
  116. package/lib/typescript/src/components/StickerPicker/StickerSectionList.d.ts +15 -0
  117. package/lib/typescript/src/components/StickerPicker/StickerSectionList.d.ts.map +1 -0
  118. package/lib/typescript/src/components/StickerPicker/constants.d.ts +11 -0
  119. package/lib/typescript/src/components/StickerPicker/constants.d.ts.map +1 -0
  120. package/lib/typescript/src/components/StickerPicker/index.d.ts +7 -0
  121. package/lib/typescript/src/components/StickerPicker/index.d.ts.map +1 -0
  122. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +3 -0
  123. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -0
  124. package/lib/typescript/src/config/api-endpoints.d.ts +4 -0
  125. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  126. package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts +2 -1
  127. package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts.map +1 -1
  128. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +5 -3
  129. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  130. package/lib/typescript/src/hooks/query-keys.d.ts +11 -0
  131. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  132. package/lib/typescript/src/hooks/resourceCenter/useThreadResources.d.ts +21 -0
  133. package/lib/typescript/src/hooks/resourceCenter/useThreadResources.d.ts.map +1 -0
  134. package/lib/typescript/src/hooks/stickers/useFetchStickerByCategory.d.ts +3 -0
  135. package/lib/typescript/src/hooks/stickers/useFetchStickerByCategory.d.ts.map +1 -0
  136. package/lib/typescript/src/hooks/stickers/useStickerCategories.d.ts +3 -0
  137. package/lib/typescript/src/hooks/stickers/useStickerCategories.d.ts.map +1 -0
  138. package/lib/typescript/src/hooks/stickers/useStickerHistory.d.ts +3 -0
  139. package/lib/typescript/src/hooks/stickers/useStickerHistory.d.ts.map +1 -0
  140. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
  141. package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
  142. package/lib/typescript/src/hooks/useMediaViewer.d.ts +5 -1
  143. package/lib/typescript/src/hooks/useMediaViewer.d.ts.map +1 -1
  144. package/lib/typescript/src/hooks/useSendAttachment.d.ts.map +1 -1
  145. package/lib/typescript/src/index.d.ts +2 -0
  146. package/lib/typescript/src/index.d.ts.map +1 -1
  147. package/lib/typescript/src/screens/MediaView/index.d.ts.map +1 -1
  148. package/lib/typescript/src/screens/ResourceCenter/FileResourceRow.d.ts +7 -0
  149. package/lib/typescript/src/screens/ResourceCenter/FileResourceRow.d.ts.map +1 -0
  150. package/lib/typescript/src/screens/ResourceCenter/GridRenderer.d.ts +14 -0
  151. package/lib/typescript/src/screens/ResourceCenter/GridRenderer.d.ts.map +1 -0
  152. package/lib/typescript/src/screens/ResourceCenter/GridThumbnail.d.ts +10 -0
  153. package/lib/typescript/src/screens/ResourceCenter/GridThumbnail.d.ts.map +1 -0
  154. package/lib/typescript/src/screens/ResourceCenter/LinkResourceRow.d.ts +8 -0
  155. package/lib/typescript/src/screens/ResourceCenter/LinkResourceRow.d.ts.map +1 -0
  156. package/lib/typescript/src/screens/ResourceCenter/ListRenderer.d.ts +16 -0
  157. package/lib/typescript/src/screens/ResourceCenter/ListRenderer.d.ts.map +1 -0
  158. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabBar.d.ts +3 -0
  159. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabBar.d.ts.map +1 -0
  160. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts +16 -0
  161. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts.map +1 -0
  162. package/lib/typescript/src/screens/ResourceCenter/ResourceEmptyState.d.ts +6 -0
  163. package/lib/typescript/src/screens/ResourceCenter/ResourceEmptyState.d.ts.map +1 -0
  164. package/lib/typescript/src/screens/ResourceCenter/index.d.ts +4 -0
  165. package/lib/typescript/src/screens/ResourceCenter/index.d.ts.map +1 -0
  166. package/lib/typescript/src/screens/ResourceCenter/types.d.ts +28 -0
  167. package/lib/typescript/src/screens/ResourceCenter/types.d.ts.map +1 -0
  168. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +2 -2
  169. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  170. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  171. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +2 -1
  172. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
  173. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +3 -0
  174. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
  175. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  176. package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
  177. package/lib/typescript/src/services/endpoints.d.ts +4 -1
  178. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  179. package/lib/typescript/src/services/resourceCenter.d.ts +9 -0
  180. package/lib/typescript/src/services/resourceCenter.d.ts.map +1 -0
  181. package/lib/typescript/src/store/conversation.d.ts +0 -3
  182. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  183. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  184. package/lib/typescript/src/types/attachment.d.ts +2 -0
  185. package/lib/typescript/src/types/attachment.d.ts.map +1 -1
  186. package/lib/typescript/src/types/chat.d.ts +66 -15
  187. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  188. package/lib/typescript/src/types/sticker.d.ts +21 -0
  189. package/lib/typescript/src/types/sticker.d.ts.map +1 -0
  190. package/lib/typescript/src/utils/conversation.d.ts +1 -2
  191. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  192. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  193. package/lib/typescript/src/utils/url.d.ts +7 -0
  194. package/lib/typescript/src/utils/url.d.ts.map +1 -1
  195. package/package.json +2 -2
  196. package/src/assets/images/index.ts +1 -0
  197. package/src/assets/images/resource-center-empty-box.png +0 -0
  198. package/src/components/MediaViewerModal.tsx +8 -3
  199. package/src/components/MergedImageGrid.tsx +1 -0
  200. package/src/components/StickerPicker/StickerCategoryTabs.tsx +103 -0
  201. package/src/components/StickerPicker/StickerPickerPlaceholder.tsx +16 -0
  202. package/src/components/StickerPicker/StickerPickerRowItem.tsx +43 -0
  203. package/src/components/StickerPicker/StickerSectionList.tsx +141 -0
  204. package/src/components/StickerPicker/constants.ts +20 -0
  205. package/src/components/StickerPicker/index.tsx +166 -0
  206. package/src/components/messages/stickerMessage/index.tsx +26 -0
  207. package/src/config/api-endpoints.ts +8 -0
  208. package/src/core/notification/pushToken.ts +7 -7
  209. package/src/hooks/conversation/useGetConversationDetail.ts +20 -1
  210. package/src/hooks/flows/useGetConversationFlowMainMenu.ts +1 -1
  211. package/src/hooks/message/useSendMessage.ts +49 -27
  212. package/src/hooks/query-keys.ts +16 -0
  213. package/src/hooks/resourceCenter/useThreadResources.ts +124 -0
  214. package/src/hooks/stickers/useFetchStickerByCategory.ts +15 -0
  215. package/src/hooks/stickers/useStickerCategories.ts +17 -0
  216. package/src/hooks/stickers/useStickerHistory.ts +19 -0
  217. package/src/hooks/useChatCompose.ts +0 -19
  218. package/src/hooks/useImageAttachment.ts +1 -2
  219. package/src/hooks/useMediaViewer.ts +21 -7
  220. package/src/hooks/useSendAttachment.ts +4 -1
  221. package/src/index.tsx +2 -0
  222. package/src/screens/MediaView/index.tsx +117 -54
  223. package/src/screens/ResourceCenter/FileResourceRow.tsx +99 -0
  224. package/src/screens/ResourceCenter/GridRenderer.tsx +104 -0
  225. package/src/screens/ResourceCenter/GridThumbnail.tsx +98 -0
  226. package/src/screens/ResourceCenter/LinkResourceRow.tsx +96 -0
  227. package/src/screens/ResourceCenter/ListRenderer.tsx +81 -0
  228. package/src/screens/ResourceCenter/ResourceCenterTabBar.tsx +89 -0
  229. package/src/screens/ResourceCenter/ResourceCenterTabScene.tsx +142 -0
  230. package/src/screens/ResourceCenter/ResourceEmptyState.tsx +39 -0
  231. package/src/screens/ResourceCenter/index.tsx +129 -0
  232. package/src/screens/ResourceCenter/types.ts +56 -0
  233. package/src/screens/chat-detail/ChatComposer.tsx +106 -19
  234. package/src/screens/chat-detail/ChatDetail.tsx +5 -19
  235. package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +21 -5
  236. package/src/screens/chat-detail/hooks/useChatComposerState.ts +7 -0
  237. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +4 -5
  238. package/src/services/attachmentOrchestrator.ts +16 -3
  239. package/src/services/endpoints.ts +6 -1
  240. package/src/services/resourceCenter.ts +137 -0
  241. package/src/store/conversation.ts +0 -24
  242. package/src/translation/resources/i18n.ts +10 -0
  243. package/src/types/attachment.ts +3 -0
  244. package/src/types/chat.ts +80 -15
  245. package/src/types/sticker.ts +23 -0
  246. package/src/utils/conversation.ts +0 -28
  247. package/src/utils/messageUtils.ts +3 -0
  248. package/src/utils/url.ts +11 -0
@@ -0,0 +1,98 @@
1
+ import { memo, useCallback, useMemo, useState } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { KContainer, KImage, KLabel, KColors } from '@droppii/libs';
4
+ import { formatDuration } from '../../utils/dateTimeUtils';
5
+ import ChatImageDimens from '../../utils/chatImageDimens';
6
+ import { DThreadResourceContentType } from '../../types/chat';
7
+ import type { DThreadResource } from '../../types/chat';
8
+
9
+ interface Props {
10
+ item: DThreadResource;
11
+ index: number;
12
+ size: number;
13
+ onPress: (index: number) => void;
14
+ }
15
+
16
+ export const GridThumbnail = memo(({ item, index, size, onPress }: Props) => {
17
+ const isVideo = item.contentType === DThreadResourceContentType.VIDEO;
18
+ const showDuration = isVideo && item.duration != null;
19
+ const durationLabel =
20
+ item.duration != null ? formatDuration(item.duration) : '';
21
+
22
+ // First mount of a cell has nothing painted yet — a bare `KImage.Base` on a
23
+ // white screen flashes as an empty gap until the (possibly not-yet-cached)
24
+ // thumbnail decodes. Same skeleton-over-gray-fallback pattern as
25
+ // `MergedImageGrid.tsx`'s chat-bubble grid.
26
+ const [isLoading, setIsLoading] = useState(true);
27
+ const onLoadEnd = useCallback(() => setIsLoading(false), []);
28
+
29
+ // Never request the original full-resolution asset for a grid cell — run
30
+ // whichever URL we end up with (BE's own resized `thumbnailUrl`, or a
31
+ // fallback to full-res `sourceUrl` if BE didn't provide one) through the
32
+ // same CDN resize+webp pipeline `imageMessage.tsx` uses for chat bubbles.
33
+ // `size` is included in deps since it changes on rotation/window resize.
34
+ const thumbnailUri = useMemo(() => {
35
+ // `size` is a sub-pixel float (screen-width / 3 columns) — round it, since
36
+ // the resize dimensions baked into the URL should be whole pixels.
37
+ const roundedSize = Math.round(size);
38
+ return ChatImageDimens.getOptimizedImageUrlWithDimensions(
39
+ item.thumbnailUrl ?? item.sourceUrl,
40
+ roundedSize,
41
+ roundedSize
42
+ );
43
+ }, [item.thumbnailUrl, item.sourceUrl, size]);
44
+
45
+ return (
46
+ <KContainer.Touchable
47
+ style={[styles.cell, { width: size, height: size }]}
48
+ onPress={() => onPress(index)}
49
+ accessibilityLabel={item.fileName || (isVideo ? 'Video' : 'Image')}
50
+ >
51
+ <KImage.Base
52
+ uri={thumbnailUri}
53
+ width={size}
54
+ height={size}
55
+ br="2x"
56
+ transition="fade"
57
+ onLoadEnd={onLoadEnd}
58
+ />
59
+ <KContainer.VisibleView visible={isLoading}>
60
+ <KContainer.View
61
+ style={[styles.skeleton, { width: size, height: size }]}
62
+ />
63
+ </KContainer.VisibleView>
64
+ <KContainer.VisibleView visible={showDuration}>
65
+ <KContainer.View style={styles.durationBadge}>
66
+ <KLabel.Text typo="Text2XsNormal" color={KColors.white}>
67
+ {durationLabel}
68
+ </KLabel.Text>
69
+ </KContainer.View>
70
+ </KContainer.VisibleView>
71
+ </KContainer.Touchable>
72
+ );
73
+ });
74
+
75
+ const styles = StyleSheet.create({
76
+ // Fallback background so a cell is never a stark blank/white gap while its
77
+ // thumbnail is still decoding.
78
+ cell: {
79
+ backgroundColor: KColors.palette.gray.w50,
80
+ overflow: 'hidden',
81
+ },
82
+ skeleton: {
83
+ position: 'absolute',
84
+ top: 0,
85
+ left: 0,
86
+ backgroundColor: KColors.palette.gray.w50,
87
+ borderRadius: 8,
88
+ },
89
+ // Figma: Video-Time — black pill, bottom-right overlay, cornerRadius 12 (KRadius '3x')
90
+ durationBadge: {
91
+ position: 'absolute',
92
+ right: 4,
93
+ bottom: 4,
94
+ borderRadius: 12,
95
+ paddingHorizontal: 4,
96
+ backgroundColor: 'rgba(0,0,0,1)',
97
+ },
98
+ });
@@ -0,0 +1,96 @@
1
+ import { memo } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import {
4
+ KContainer,
5
+ KImage,
6
+ KLabel,
7
+ KColors,
8
+ KSpacingValue,
9
+ } from '@droppii/libs';
10
+ import type { DThreadResource } from '../../types/chat';
11
+
12
+ interface Props {
13
+ item: DThreadResource;
14
+ onPressUrl?: (url: string) => void;
15
+ }
16
+
17
+ export const LinkResourceRow = memo(({ item, onPressUrl }: Props) => (
18
+ <KContainer.Touchable
19
+ style={styles.row}
20
+ br="3x"
21
+ background={KColors.opacity.grayLighter[20]}
22
+ onPress={() => item.linkUrl && onPressUrl?.(item.linkUrl)}
23
+ >
24
+ <KContainer.View style={styles.thumb}>
25
+ <KContainer.VisibleView visible={!!item.linkImageUrl}>
26
+ <KImage.Base
27
+ uri={item.linkImageUrl ?? ''}
28
+ width={76}
29
+ height={76}
30
+ br="2x"
31
+ resizeMode="cover"
32
+ />
33
+ </KContainer.VisibleView>
34
+ {/* Figma "Link-No-Img" fallback: gray.w200 background, cornerRadius 12,
35
+ white generic link icon — NOT the same styling as the image variant.
36
+ `KContainer.VisibleView` only takes `visible`/`children` (no `style`
37
+ passthrough — confirmed in its source), so the fallback box needs its
38
+ own `KContainer.View` wrapper inside it. */}
39
+ <KContainer.VisibleView visible={!item.linkImageUrl}>
40
+ <KContainer.View style={styles.thumbFallback}>
41
+ <KImage.VectorIcons name="link-alt-o" color={KColors.white} />
42
+ </KContainer.View>
43
+ </KContainer.VisibleView>
44
+ </KContainer.View>
45
+ <KContainer.View flexS={1}>
46
+ <KContainer.VisibleView visible={!!item.linkDomain}>
47
+ <KLabel.Text
48
+ // ⚠️ U7: Figma domain-line color (#5C6366) has no exact @droppii/libs
49
+ // token match — using closest system token (gray.w500) pending Designer confirm.
50
+ typo="TextNmNormal"
51
+ color={KColors.palette.gray.w500}
52
+ numberOfLines={1}
53
+ >
54
+ {item.linkDomain}
55
+ </KLabel.Text>
56
+ </KContainer.VisibleView>
57
+ <KContainer.VisibleView visible={!!item.linkTitle}>
58
+ <KLabel.Text
59
+ typo="TextMdMedium"
60
+ color={KColors.palette.gray.w900}
61
+ numberOfLines={2}
62
+ >
63
+ {item.linkTitle}
64
+ </KLabel.Text>
65
+ </KContainer.VisibleView>
66
+ {/* U8 re-resolved via pixel check against `14208:88701`: the description
67
+ text layer exists with real content in every sample row, but is
68
+ `visible: false` in ALL of them — the shipped design never shows it,
69
+ even though BE provides `linkDescription`. Dropped per pixel-perfect
70
+ decision (not rendered), despite the field still being populated. */}
71
+ </KContainer.View>
72
+ </KContainer.Touchable>
73
+ ));
74
+
75
+ const styles = StyleSheet.create({
76
+ row: {
77
+ flexDirection: 'row',
78
+ alignItems: 'center',
79
+ gap: KSpacingValue['0.5rem'],
80
+ padding: KSpacingValue['0.75rem'],
81
+ marginBottom: KSpacingValue['0.5rem'],
82
+ },
83
+ // Figma Link-Item: thumbnail slot is 76x76 (not 56x56).
84
+ thumb: {
85
+ width: 76,
86
+ height: 76,
87
+ },
88
+ thumbFallback: {
89
+ width: 76,
90
+ height: 76,
91
+ borderRadius: 12,
92
+ justifyContent: 'center',
93
+ alignItems: 'center',
94
+ backgroundColor: KColors.palette.gray.w200,
95
+ },
96
+ });
@@ -0,0 +1,81 @@
1
+ import { memo, useCallback } from 'react';
2
+ import type { ReactElement } from 'react';
3
+ import { ActivityIndicator, StyleSheet } from 'react-native';
4
+ import { LegendList } from '@legendapp/list/react-native';
5
+ import { KContainer, KColors, KSpacingValue } from '@droppii/libs';
6
+ import { FileResourceRow } from './FileResourceRow';
7
+ import { LinkResourceRow } from './LinkResourceRow';
8
+ import { DThreadResourceContentType } from '../../types/chat';
9
+ import type { DThreadResource } from '../../types/chat';
10
+
11
+ interface Props {
12
+ items: DThreadResource[];
13
+ contentType: DThreadResourceContentType;
14
+ onEndReached: () => void;
15
+ isFetchingNextPage: boolean;
16
+ onPressUrl?: (url: string) => void;
17
+ onRefresh: () => void;
18
+ isRefetching: boolean;
19
+ emptyComponent: ReactElement;
20
+ }
21
+
22
+ const ROW_ESTIMATE = 80;
23
+
24
+ export const ListRenderer = memo(
25
+ ({
26
+ items,
27
+ contentType,
28
+ onEndReached,
29
+ isFetchingNextPage,
30
+ onPressUrl,
31
+ onRefresh,
32
+ isRefetching,
33
+ emptyComponent,
34
+ }: Props) => {
35
+ const isLink = contentType === DThreadResourceContentType.URL_TEXT;
36
+
37
+ const renderItem = useCallback(
38
+ ({ item }: { item: DThreadResource }) =>
39
+ isLink ? (
40
+ <LinkResourceRow item={item} onPressUrl={onPressUrl} />
41
+ ) : (
42
+ <FileResourceRow item={item} />
43
+ ),
44
+ [isLink, onPressUrl]
45
+ );
46
+
47
+ const keyExtractor = useCallback((item: DThreadResource) => item.id, []);
48
+
49
+ return (
50
+ <LegendList
51
+ data={items}
52
+ renderItem={renderItem}
53
+ keyExtractor={keyExtractor}
54
+ estimatedItemSize={ROW_ESTIMATE}
55
+ contentContainerStyle={styles.content}
56
+ onEndReached={onEndReached}
57
+ onEndReachedThreshold={0.5}
58
+ onRefresh={onRefresh}
59
+ refreshing={isRefetching}
60
+ ListEmptyComponent={emptyComponent}
61
+ ListFooterComponent={
62
+ <KContainer.VisibleView visible={isFetchingNextPage}>
63
+ <ActivityIndicator
64
+ style={styles.footer}
65
+ color={KColors.palette.primary.w400}
66
+ />
67
+ </KContainer.VisibleView>
68
+ }
69
+ />
70
+ );
71
+ }
72
+ );
73
+
74
+ const styles = StyleSheet.create({
75
+ content: {
76
+ padding: KSpacingValue['0.75rem'],
77
+ },
78
+ footer: {
79
+ paddingVertical: KSpacingValue['1rem'],
80
+ },
81
+ });
@@ -0,0 +1,89 @@
1
+ import { useMemo } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import {
4
+ TabBar,
5
+ type Route,
6
+ type TabBarProps,
7
+ type TabDescriptor,
8
+ } from 'react-native-tab-view';
9
+ import { KLabel, KColors, KSpacingValue } from '@droppii/libs';
10
+
11
+ // Stable module-level reference — must NOT be defined inside the component body
12
+ // (or inside the `useMemo` below), or React treats it as a new type every render.
13
+ const renderTabLabel: NonNullable<TabDescriptor<Route>['label']> = ({
14
+ labelText,
15
+ color,
16
+ }) => (
17
+ <KLabel.Text typo="TextNmBold" color={color}>
18
+ {labelText}
19
+ </KLabel.Text>
20
+ );
21
+
22
+ // Uses `react-native-tab-view`'s own `TabBar` (animated sliding indicator,
23
+ // built-in accessibility/ripple) instead of a hand-rolled tab row — only the
24
+ // design tokens are custom (colors/indicator/label typography), matching the
25
+ // Figma underline-tab spec via the library's supported customization surface
26
+ // (`activeColor`/`inactiveColor`/`indicatorStyle`/per-route `label`).
27
+ //
28
+ // NOT wrapped in `React.memo`: `TabView` calls `renderTabBar(props)` as a plain
29
+ // function (not `<Comp/>` JSX — see `TabView.js`: `renderTabBar({...})`), and a
30
+ // `memo()`-wrapped component is a React element object, not a callable function.
31
+ // Passing the memoized object here throws `renderTabBar is not a function`.
32
+ export function ResourceCenterTabBar<T extends Route>(props: TabBarProps<T>) {
33
+ const options = useMemo<Record<string, TabDescriptor<T>>>(
34
+ () =>
35
+ Object.fromEntries(
36
+ props.navigationState.routes.map((route) => [
37
+ route.key,
38
+ {
39
+ labelText: (route as Route & { title?: string }).title,
40
+ label: renderTabLabel,
41
+ } satisfies TabDescriptor<T>,
42
+ ])
43
+ ),
44
+ [props.navigationState.routes]
45
+ );
46
+
47
+ return (
48
+ <TabBar
49
+ {...props}
50
+ scrollEnabled
51
+ options={options}
52
+ activeColor={KColors.palette.primary.w400}
53
+ inactiveColor={KColors.palette.gray.w400}
54
+ pressColor="transparent"
55
+ style={styles.bar}
56
+ tabStyle={styles.tab}
57
+ indicatorStyle={styles.indicator}
58
+ />
59
+ );
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ // Figma `Tab-Bar` (14208:92918): solid white background + 1px bottom hairline
64
+ // (`gray.w25` #EFEFEF) — no shadow/elevation.
65
+ bar: {
66
+ backgroundColor: KColors.white,
67
+ borderBottomWidth: 1,
68
+ borderBottomColor: KColors.palette.gray.w25,
69
+ shadowOpacity: 0,
70
+ elevation: 0,
71
+ },
72
+ // `width: 'auto'` is REQUIRED alongside `scrollEnabled` above — without it,
73
+ // `TabBar`'s internal `getComputedTabWidth` falls back to `layout.width / 5 * 2`
74
+ // (40% of screen width per tab) before it has measured anything, rendering
75
+ // hugely oversized tabs. `'auto'` makes it use each tab's real measured label
76
+ // width instead (`tabWidths[route.key]`) — matches Figma's hug-content tabs
77
+ // (widths 53/65/73/81pt), fixed to each tab's own text, not stretched/guessed.
78
+ tab: {
79
+ width: 'auto',
80
+ paddingHorizontal: KSpacingValue['0.75rem'],
81
+ minHeight: 0,
82
+ },
83
+ // Figma: 3px bar, cornerRadius 4, primary blue.
84
+ indicator: {
85
+ backgroundColor: KColors.palette.primary.w400,
86
+ height: 3,
87
+ borderRadius: 4,
88
+ },
89
+ });
@@ -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
+ });