@droppii-org/chat-mobile 0.2.50 → 0.2.51

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 (137) hide show
  1. package/lib/module/components/StickerPicker/StickerCategoryTabs.js +86 -0
  2. package/lib/module/components/StickerPicker/StickerCategoryTabs.js.map +1 -0
  3. package/lib/module/components/StickerPicker/StickerPickerPlaceholder.js +18 -0
  4. package/lib/module/components/StickerPicker/StickerPickerPlaceholder.js.map +1 -0
  5. package/lib/module/components/StickerPicker/StickerPickerRowItem.js +35 -0
  6. package/lib/module/components/StickerPicker/StickerPickerRowItem.js.map +1 -0
  7. package/lib/module/components/StickerPicker/StickerSectionList.js +110 -0
  8. package/lib/module/components/StickerPicker/StickerSectionList.js.map +1 -0
  9. package/lib/module/components/StickerPicker/constants.js +14 -0
  10. package/lib/module/components/StickerPicker/constants.js.map +1 -0
  11. package/lib/module/components/StickerPicker/index.js +134 -0
  12. package/lib/module/components/StickerPicker/index.js.map +1 -0
  13. package/lib/module/components/messages/stickerMessage/index.js +28 -0
  14. package/lib/module/components/messages/stickerMessage/index.js.map +1 -0
  15. package/lib/module/config/api-endpoints.js +4 -1
  16. package/lib/module/config/api-endpoints.js.map +1 -1
  17. package/lib/module/hooks/conversation/useGetConversationDetail.js +15 -0
  18. package/lib/module/hooks/conversation/useGetConversationDetail.js.map +1 -1
  19. package/lib/module/hooks/flows/useGetConversationFlowMainMenu.js +1 -1
  20. package/lib/module/hooks/flows/useGetConversationFlowMainMenu.js.map +1 -1
  21. package/lib/module/hooks/message/useSendMessage.js +45 -18
  22. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  23. package/lib/module/hooks/query-keys.js +7 -0
  24. package/lib/module/hooks/query-keys.js.map +1 -1
  25. package/lib/module/hooks/stickers/useFetchStickerByCategory.js +12 -0
  26. package/lib/module/hooks/stickers/useFetchStickerByCategory.js.map +1 -0
  27. package/lib/module/hooks/stickers/useStickerCategories.js +14 -0
  28. package/lib/module/hooks/stickers/useStickerCategories.js.map +1 -0
  29. package/lib/module/hooks/stickers/useStickerHistory.js +16 -0
  30. package/lib/module/hooks/stickers/useStickerHistory.js.map +1 -0
  31. package/lib/module/hooks/useChatCompose.js +1 -18
  32. package/lib/module/hooks/useChatCompose.js.map +1 -1
  33. package/lib/module/screens/chat-detail/ChatComposer.js +64 -16
  34. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  35. package/lib/module/screens/chat-detail/ChatDetail.js +6 -20
  36. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  37. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +15 -4
  38. package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
  39. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +3 -0
  40. package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
  41. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +5 -6
  42. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  43. package/lib/module/services/attachmentOrchestrator.js +1 -1
  44. package/lib/module/services/attachmentOrchestrator.js.map +1 -1
  45. package/lib/module/services/endpoints.js +4 -1
  46. package/lib/module/services/endpoints.js.map +1 -1
  47. package/lib/module/store/conversation.js +1 -20
  48. package/lib/module/store/conversation.js.map +1 -1
  49. package/lib/module/translation/resources/i18n.js +3 -1
  50. package/lib/module/translation/resources/i18n.js.map +1 -1
  51. package/lib/module/types/chat.js +6 -0
  52. package/lib/module/types/chat.js.map +1 -1
  53. package/lib/module/types/sticker.js +2 -0
  54. package/lib/module/types/sticker.js.map +1 -0
  55. package/lib/module/utils/conversation.js +1 -24
  56. package/lib/module/utils/conversation.js.map +1 -1
  57. package/lib/module/utils/messageUtils.js +2 -0
  58. package/lib/module/utils/messageUtils.js.map +1 -1
  59. package/lib/typescript/src/components/StickerPicker/StickerCategoryTabs.d.ts +9 -0
  60. package/lib/typescript/src/components/StickerPicker/StickerCategoryTabs.d.ts.map +1 -0
  61. package/lib/typescript/src/components/StickerPicker/StickerPickerPlaceholder.d.ts +2 -0
  62. package/lib/typescript/src/components/StickerPicker/StickerPickerPlaceholder.d.ts.map +1 -0
  63. package/lib/typescript/src/components/StickerPicker/StickerPickerRowItem.d.ts +8 -0
  64. package/lib/typescript/src/components/StickerPicker/StickerPickerRowItem.d.ts.map +1 -0
  65. package/lib/typescript/src/components/StickerPicker/StickerSectionList.d.ts +15 -0
  66. package/lib/typescript/src/components/StickerPicker/StickerSectionList.d.ts.map +1 -0
  67. package/lib/typescript/src/components/StickerPicker/constants.d.ts +11 -0
  68. package/lib/typescript/src/components/StickerPicker/constants.d.ts.map +1 -0
  69. package/lib/typescript/src/components/StickerPicker/index.d.ts +7 -0
  70. package/lib/typescript/src/components/StickerPicker/index.d.ts.map +1 -0
  71. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +3 -0
  72. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -0
  73. package/lib/typescript/src/config/api-endpoints.d.ts +3 -0
  74. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  75. package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts +2 -1
  76. package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts.map +1 -1
  77. package/lib/typescript/src/hooks/message/useSendMessage.d.ts +5 -3
  78. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  79. package/lib/typescript/src/hooks/query-keys.d.ts +7 -0
  80. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  81. package/lib/typescript/src/hooks/stickers/useFetchStickerByCategory.d.ts +3 -0
  82. package/lib/typescript/src/hooks/stickers/useFetchStickerByCategory.d.ts.map +1 -0
  83. package/lib/typescript/src/hooks/stickers/useStickerCategories.d.ts +3 -0
  84. package/lib/typescript/src/hooks/stickers/useStickerCategories.d.ts.map +1 -0
  85. package/lib/typescript/src/hooks/stickers/useStickerHistory.d.ts +3 -0
  86. package/lib/typescript/src/hooks/stickers/useStickerHistory.d.ts.map +1 -0
  87. package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
  88. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +2 -2
  89. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  90. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  91. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +2 -1
  92. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +3 -0
  94. package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
  95. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  96. package/lib/typescript/src/services/endpoints.d.ts +4 -1
  97. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  98. package/lib/typescript/src/store/conversation.d.ts +0 -3
  99. package/lib/typescript/src/store/conversation.d.ts.map +1 -1
  100. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  101. package/lib/typescript/src/types/chat.d.ts +12 -19
  102. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  103. package/lib/typescript/src/types/sticker.d.ts +21 -0
  104. package/lib/typescript/src/types/sticker.d.ts.map +1 -0
  105. package/lib/typescript/src/utils/conversation.d.ts +1 -2
  106. package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
  107. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  108. package/package.json +2 -2
  109. package/src/components/StickerPicker/StickerCategoryTabs.tsx +103 -0
  110. package/src/components/StickerPicker/StickerPickerPlaceholder.tsx +16 -0
  111. package/src/components/StickerPicker/StickerPickerRowItem.tsx +43 -0
  112. package/src/components/StickerPicker/StickerSectionList.tsx +141 -0
  113. package/src/components/StickerPicker/constants.ts +20 -0
  114. package/src/components/StickerPicker/index.tsx +166 -0
  115. package/src/components/messages/stickerMessage/index.tsx +26 -0
  116. package/src/config/api-endpoints.ts +6 -0
  117. package/src/hooks/conversation/useGetConversationDetail.ts +20 -1
  118. package/src/hooks/flows/useGetConversationFlowMainMenu.ts +1 -1
  119. package/src/hooks/message/useSendMessage.ts +49 -27
  120. package/src/hooks/query-keys.ts +10 -0
  121. package/src/hooks/stickers/useFetchStickerByCategory.ts +15 -0
  122. package/src/hooks/stickers/useStickerCategories.ts +17 -0
  123. package/src/hooks/stickers/useStickerHistory.ts +19 -0
  124. package/src/hooks/useChatCompose.ts +0 -19
  125. package/src/screens/chat-detail/ChatComposer.tsx +106 -19
  126. package/src/screens/chat-detail/ChatDetail.tsx +5 -19
  127. package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +21 -5
  128. package/src/screens/chat-detail/hooks/useChatComposerState.ts +7 -0
  129. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +4 -5
  130. package/src/services/attachmentOrchestrator.ts +1 -1
  131. package/src/services/endpoints.ts +6 -1
  132. package/src/store/conversation.ts +0 -24
  133. package/src/translation/resources/i18n.ts +3 -0
  134. package/src/types/chat.ts +13 -21
  135. package/src/types/sticker.ts +23 -0
  136. package/src/utils/conversation.ts +0 -28
  137. package/src/utils/messageUtils.ts +3 -0
@@ -0,0 +1,103 @@
1
+ import { memo, useEffect, useRef } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import {
4
+ KContainer,
5
+ KImage,
6
+ KColors,
7
+ KSpacingValue,
8
+ KRadiusValue,
9
+ } from '@droppii/libs';
10
+ import type { StickerCategory } from '../../types/sticker';
11
+ import { CATEGORY_TAB_HEIGHT, RECENT_CATEGORY_ID } from './constants';
12
+
13
+ interface Props {
14
+ categories: StickerCategory[];
15
+ activeCategory: string;
16
+ onSelectCategory: (id: string) => void;
17
+ }
18
+
19
+ export const StickerCategoryTabs = memo(
20
+ ({ categories, activeCategory, onSelectCategory }: Props) => {
21
+ const listRef = useRef<any>(null);
22
+ const pressedFromHereRef = useRef(false);
23
+
24
+ useEffect(() => {
25
+ if (pressedFromHereRef.current) {
26
+ pressedFromHereRef.current = false;
27
+ return;
28
+ }
29
+ const index = categories.findIndex((c) => c.id === activeCategory);
30
+ if (index >= 0 && listRef.current) {
31
+ try {
32
+ listRef.current.scrollToIndex({
33
+ index,
34
+ animated: true,
35
+ viewPosition: 0.5,
36
+ });
37
+ } catch {}
38
+ }
39
+ }, [activeCategory, categories]);
40
+
41
+ return (
42
+ <KContainer.FlatList
43
+ ref={listRef}
44
+ data={categories}
45
+ horizontal
46
+ showsHorizontalScrollIndicator={false}
47
+ keyExtractor={(item: StickerCategory) => item.id}
48
+ renderItem={({ item }: { item: StickerCategory }) => {
49
+ const isActive = item.id === activeCategory;
50
+ return (
51
+ <KContainer.Touchable
52
+ onPress={() => {
53
+ pressedFromHereRef.current = true;
54
+ onSelectCategory(item.id);
55
+ }}
56
+ center
57
+ style={[styles.tab, isActive && styles.tabActive]}
58
+ >
59
+ {item?.id === RECENT_CATEGORY_ID ? (
60
+ <KImage.VectorIcons name="time-circle-b" size={28} />
61
+ ) : (
62
+ item?.icon && (
63
+ <KImage.Base
64
+ uri={item?.icon?.fullUrl}
65
+ width={28}
66
+ height={28}
67
+ />
68
+ )
69
+ )}
70
+ </KContainer.Touchable>
71
+ );
72
+ }}
73
+ style={styles.list}
74
+ contentContainerStyle={styles.content}
75
+ />
76
+ );
77
+ }
78
+ );
79
+
80
+ const styles = StyleSheet.create({
81
+ list: {
82
+ height: CATEGORY_TAB_HEIGHT,
83
+ },
84
+ content: {
85
+ alignItems: 'center',
86
+ paddingHorizontal: KSpacingValue['0.75rem'],
87
+ gap: KSpacingValue['0.75rem'],
88
+ },
89
+ tab: {
90
+ justifyContent: 'center',
91
+ alignItems: 'center',
92
+ padding: KSpacingValue['0.25rem'],
93
+ },
94
+ tabActive: {
95
+ borderRadius: KRadiusValue['2x'],
96
+ backgroundColor: KColors.opacity.grayDark[10],
97
+ borderWidth: 1,
98
+ borderColor: KColors.palette.gray.w700,
99
+ },
100
+ recentIcon: {
101
+ fontSize: 22,
102
+ },
103
+ });
@@ -0,0 +1,16 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { KColors, KContainer, KRadiusValue } from '@droppii/libs';
3
+ import { STICKER_SIZE } from './constants';
4
+
5
+ export const StickerPickerPlaceholder = () => (
6
+ <KContainer.View style={styles.placeholder} />
7
+ );
8
+
9
+ const styles = StyleSheet.create({
10
+ placeholder: {
11
+ width: STICKER_SIZE,
12
+ height: STICKER_SIZE,
13
+ borderRadius: KRadiusValue['2x'],
14
+ backgroundColor: KColors.border.light,
15
+ },
16
+ });
@@ -0,0 +1,43 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { KContainer, KImage } from '@droppii/libs';
3
+ import { STICKER_SIZE, STICKER_GAP } from './constants';
4
+ import type { StickerItem, StickerRow } from '../../types/sticker';
5
+ import { StickerPickerPlaceholder } from './StickerPickerPlaceholder';
6
+
7
+ interface Props {
8
+ row: StickerRow;
9
+ onSelectSticker: (sticker: StickerItem) => void;
10
+ }
11
+
12
+ export const StickerPickerRowItem = ({ row, onSelectSticker }: Props) => (
13
+ <KContainer.View row style={styles.row}>
14
+ {row.map((sticker, i) => (
15
+ <KContainer.View
16
+ key={i}
17
+ style={i < row.length - 1 ? styles.cell : undefined}
18
+ >
19
+ {sticker ? (
20
+ <KContainer.Touchable onPress={() => onSelectSticker(sticker)}>
21
+ <KImage.Base
22
+ uri={sticker.file.fullUrl}
23
+ width={STICKER_SIZE}
24
+ height={STICKER_SIZE}
25
+ />
26
+ </KContainer.Touchable>
27
+ ) : (
28
+ <StickerPickerPlaceholder />
29
+ )}
30
+ </KContainer.View>
31
+ ))}
32
+ </KContainer.View>
33
+ );
34
+
35
+ const styles = StyleSheet.create({
36
+ row: {
37
+ marginBottom: STICKER_GAP,
38
+ paddingHorizontal: 12,
39
+ },
40
+ cell: {
41
+ marginRight: STICKER_GAP,
42
+ },
43
+ });
@@ -0,0 +1,141 @@
1
+ import { forwardRef, useCallback, useRef } from 'react';
2
+ import {
3
+ SectionList,
4
+ StyleSheet,
5
+ type NativeSyntheticEvent,
6
+ type NativeScrollEvent,
7
+ } from 'react-native';
8
+ import { KColors, KLabel, KSpacingValue } from '@droppii/libs';
9
+ import type { StickerItem, StickerRow } from '../../types/sticker';
10
+ import { StickerPickerRowItem } from './StickerPickerRowItem';
11
+ import { STICKER_PICKER_CONTENT_HEIGHT } from './constants';
12
+
13
+ export interface StickerPickerSection {
14
+ categoryId: string;
15
+ title: string;
16
+ data: StickerRow[];
17
+ }
18
+
19
+ interface Props {
20
+ sections: StickerPickerSection[];
21
+ onSectionVisible: (categoryId: string) => void;
22
+ onActiveCategoryChange: (categoryId: string) => void;
23
+ onSelectSticker: (sticker: StickerItem) => void;
24
+ }
25
+
26
+ const AT_BOTTOM_THRESHOLD = 8;
27
+
28
+ const VIEWABILITY_CONFIG = { itemVisiblePercentThreshold: 10 };
29
+
30
+ export const StickerSectionList = forwardRef<any, Props>(
31
+ (
32
+ { sections, onSectionVisible, onActiveCategoryChange, onSelectSticker },
33
+ ref
34
+ ) => {
35
+ const onSectionVisibleRef = useRef(onSectionVisible);
36
+ onSectionVisibleRef.current = onSectionVisible;
37
+
38
+ const onActiveCategoryChangeRef = useRef(onActiveCategoryChange);
39
+ onActiveCategoryChangeRef.current = onActiveCategoryChange;
40
+
41
+ const sectionsRef = useRef(sections);
42
+ sectionsRef.current = sections;
43
+
44
+ const isAtBottomRef = useRef(false);
45
+
46
+ const onScroll = useCallback(
47
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
48
+ const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
49
+ isAtBottomRef.current =
50
+ contentOffset.y + layoutMeasurement.height >=
51
+ contentSize.height - AT_BOTTOM_THRESHOLD;
52
+ },
53
+ []
54
+ );
55
+
56
+ const renderItem = useCallback(
57
+ ({ item }: { item: StickerRow; section: StickerPickerSection }) => (
58
+ <StickerPickerRowItem row={item} onSelectSticker={onSelectSticker} />
59
+ ),
60
+ [onSelectSticker]
61
+ );
62
+
63
+ const renderSectionHeader = useCallback(
64
+ ({ section }: { section: StickerPickerSection }) => (
65
+ <KLabel.Text
66
+ typo="TitleSection"
67
+ style={styles.sectionHeader}
68
+ color={KColors.gray.light}
69
+ >
70
+ {section.title}
71
+ </KLabel.Text>
72
+ ),
73
+ []
74
+ );
75
+
76
+ const onViewableItemsChanged = useCallback((args: any) => {
77
+ const { viewableItems } = args;
78
+ if (!viewableItems.length) return;
79
+
80
+ // Trigger lazy load for all currently visible sections
81
+ const seenCategories = new Set<string>();
82
+ viewableItems.forEach((vi: any) => {
83
+ const cat = (vi.section as StickerPickerSection)?.categoryId;
84
+ if (cat && !seenCategories.has(cat)) {
85
+ seenCategories.add(cat);
86
+ onSectionVisibleRef.current(cat);
87
+ }
88
+ });
89
+
90
+ // When at the bottom of the list, force the last section as active.
91
+ // This handles the edge case where the last section has few items and
92
+ // its header never reaches the top of the viewport.
93
+ if (isAtBottomRef.current) {
94
+ const lastSection = sectionsRef.current[sectionsRef.current.length - 1];
95
+ if (lastSection) {
96
+ onActiveCategoryChangeRef.current(lastSection.categoryId);
97
+ }
98
+ return;
99
+ }
100
+
101
+ // Otherwise use the first visible item's section (symmetric for both
102
+ // scroll directions: switches when the section header enters from either end).
103
+ const firstVisible = viewableItems.find((vi: any) => vi.section != null);
104
+ const categoryId = (firstVisible?.section as StickerPickerSection)
105
+ ?.categoryId;
106
+ if (categoryId) {
107
+ onActiveCategoryChangeRef.current(categoryId);
108
+ }
109
+ }, []);
110
+
111
+ return (
112
+ <SectionList
113
+ ref={ref}
114
+ sections={sections}
115
+ renderItem={renderItem}
116
+ renderSectionHeader={renderSectionHeader}
117
+ keyExtractor={(_, index) => String(index)}
118
+ onScroll={onScroll}
119
+ scrollEventThrottle={16}
120
+ onViewableItemsChanged={onViewableItemsChanged}
121
+ viewabilityConfig={VIEWABILITY_CONFIG}
122
+ removeClippedSubviews
123
+ initialNumToRender={8}
124
+ maxToRenderPerBatch={8}
125
+ style={styles.list}
126
+ showsVerticalScrollIndicator={false}
127
+ stickySectionHeadersEnabled={false}
128
+ />
129
+ );
130
+ }
131
+ );
132
+
133
+ const styles = StyleSheet.create({
134
+ list: {
135
+ height: STICKER_PICKER_CONTENT_HEIGHT,
136
+ },
137
+ sectionHeader: {
138
+ paddingHorizontal: KSpacingValue['0.75rem'],
139
+ paddingVertical: KSpacingValue['0.25rem'],
140
+ },
141
+ });
@@ -0,0 +1,20 @@
1
+ import { KDims } from '@droppii/libs';
2
+
3
+ export const STICKER_COLUMNS = 4;
4
+ export const STICKER_HORIZONTAL_PADDING = 12;
5
+ export const STICKER_GAP = 8;
6
+
7
+ export const STICKER_SIZE = Math.floor(
8
+ (KDims.width -
9
+ STICKER_HORIZONTAL_PADDING * 2 -
10
+ STICKER_GAP * (STICKER_COLUMNS - 1)) /
11
+ STICKER_COLUMNS
12
+ );
13
+ export const STICKER_ROW_HEIGHT = STICKER_SIZE + STICKER_GAP;
14
+ export const CATEGORY_TAB_HEIGHT = 52;
15
+ export const STICKER_SECTION_HEADER_HEIGHT = 36;
16
+ export const STICKER_PICKER_CONTENT_HEIGHT = 260;
17
+ export const STICKER_PICKER_HEIGHT =
18
+ CATEGORY_TAB_HEIGHT + STICKER_PICKER_CONTENT_HEIGHT;
19
+
20
+ export const RECENT_CATEGORY_ID = 'recent';
@@ -0,0 +1,166 @@
1
+ import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
2
+ import { KColors, KContainer } from '@droppii/libs';
3
+ import type {
4
+ StickerCategory,
5
+ StickerItem,
6
+ StickerRow,
7
+ } from '../../types/sticker';
8
+ import { useStickerCategories } from '../../hooks/stickers/useStickerCategories';
9
+ import { useStickerHistory } from '../../hooks/stickers/useStickerHistory';
10
+ import { useFetchStickerByCategory } from '../../hooks/stickers/useFetchStickerByCategory';
11
+ import {
12
+ RECENT_CATEGORY_ID,
13
+ STICKER_COLUMNS,
14
+ STICKER_PICKER_HEIGHT,
15
+ } from './constants';
16
+ import { StickerCategoryTabs } from './StickerCategoryTabs';
17
+ import {
18
+ StickerSectionList,
19
+ type StickerPickerSection,
20
+ } from './StickerSectionList';
21
+ import { trans } from '../../translation';
22
+
23
+ interface Props {
24
+ onSelectSticker: (sticker: StickerItem) => void;
25
+ }
26
+
27
+ const chunkIntoRows = (items: StickerItem[], columns: number): StickerRow[] => {
28
+ const rows: StickerRow[] = [];
29
+ for (let i = 0; i < items.length; i += columns) {
30
+ rows.push(items.slice(i, i + columns));
31
+ }
32
+ return rows;
33
+ };
34
+
35
+ const makePlaceholderRows = (count: number, columns: number): StickerRow[] => {
36
+ const rows: StickerRow[] = [];
37
+ for (let i = 0; i < count; i += columns) {
38
+ const rowSize = Math.min(columns, count - i);
39
+ rows.push(Array.from({ length: rowSize }, () => null));
40
+ }
41
+ return rows;
42
+ };
43
+
44
+ export const StickerPicker = ({ onSelectSticker }: Props) => {
45
+ const { data: categoriesData } = useStickerCategories();
46
+ const { data: recentStickers } = useStickerHistory();
47
+ const { mutateAsync: fetchStickersByCategory } = useFetchStickerByCategory();
48
+
49
+ const allCategories = useMemo<StickerCategory[]>(() => {
50
+ const recent: StickerCategory = {
51
+ id: RECENT_CATEGORY_ID,
52
+ name: trans('recent'),
53
+ icon: null,
54
+ stickerCount: 0,
55
+ };
56
+ return [recent, ...(categoriesData || [])];
57
+ }, [categoriesData]);
58
+
59
+ const [activeCategory, setActiveCategory] = useState(RECENT_CATEGORY_ID);
60
+ const [sectionDataMap, setSectionDataMap] = useState<
61
+ Record<string, StickerRow[]>
62
+ >({});
63
+
64
+ const loadedIdsRef = useRef(new Set<string>([RECENT_CATEGORY_ID]));
65
+ const sectionListRef = useRef<any>(null);
66
+ const suppressCategoryChangeRef = useRef(false);
67
+ const suppressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
68
+
69
+ // Populate recent section when history data arrives
70
+ useEffect(() => {
71
+ if (recentStickers) {
72
+ setSectionDataMap((prev) => ({
73
+ ...prev,
74
+ [RECENT_CATEGORY_ID]: chunkIntoRows(recentStickers, STICKER_COLUMNS),
75
+ }));
76
+ }
77
+ }, [recentStickers]);
78
+
79
+ // Cleanup suppress timer on unmount
80
+ useEffect(() => {
81
+ return () => {
82
+ if (suppressTimerRef.current) clearTimeout(suppressTimerRef.current);
83
+ };
84
+ }, []);
85
+
86
+ const sections = useMemo<StickerPickerSection[]>(
87
+ () =>
88
+ allCategories.map((cat) => ({
89
+ categoryId: cat.id,
90
+ title: cat.name,
91
+ data:
92
+ sectionDataMap[cat.id] ??
93
+ makePlaceholderRows(cat.stickerCount, STICKER_COLUMNS),
94
+ })),
95
+ [allCategories, sectionDataMap]
96
+ );
97
+
98
+ const handleSectionVisible = useCallback(
99
+ async (categoryId: string) => {
100
+ if (categoryId === RECENT_CATEGORY_ID) return;
101
+ if (loadedIdsRef.current.has(categoryId)) return;
102
+ loadedIdsRef.current.add(categoryId);
103
+
104
+ try {
105
+ const stickers = await fetchStickersByCategory(categoryId);
106
+ setSectionDataMap((prev) => ({
107
+ ...prev,
108
+ [categoryId]: chunkIntoRows(stickers, STICKER_COLUMNS),
109
+ }));
110
+ } catch {
111
+ loadedIdsRef.current.delete(categoryId);
112
+ }
113
+ },
114
+ [fetchStickersByCategory]
115
+ );
116
+
117
+ const handleActiveCategoryChange = useCallback((categoryId: string) => {
118
+ if (suppressCategoryChangeRef.current) return;
119
+ setActiveCategory(categoryId);
120
+ }, []);
121
+
122
+ const handleCategoryPress = useCallback(
123
+ (categoryId: string) => {
124
+ setActiveCategory(categoryId);
125
+
126
+ suppressCategoryChangeRef.current = true;
127
+ if (suppressTimerRef.current) clearTimeout(suppressTimerRef.current);
128
+ suppressTimerRef.current = setTimeout(() => {
129
+ suppressCategoryChangeRef.current = false;
130
+ }, 600);
131
+
132
+ const sectionIndex = allCategories.findIndex((c) => c.id === categoryId);
133
+ if (sectionIndex >= 0 && sectionListRef.current) {
134
+ try {
135
+ sectionListRef.current.scrollToLocation({
136
+ sectionIndex,
137
+ itemIndex: 0,
138
+ animated: true,
139
+ viewOffset: 0,
140
+ });
141
+ } catch {}
142
+ }
143
+ },
144
+ [allCategories]
145
+ );
146
+
147
+ return (
148
+ <KContainer.View
149
+ style={{ height: STICKER_PICKER_HEIGHT }}
150
+ background={KColors.white}
151
+ >
152
+ <StickerCategoryTabs
153
+ categories={allCategories}
154
+ activeCategory={activeCategory}
155
+ onSelectCategory={handleCategoryPress}
156
+ />
157
+ <StickerSectionList
158
+ ref={sectionListRef}
159
+ sections={sections}
160
+ onSectionVisible={handleSectionVisible}
161
+ onActiveCategoryChange={handleActiveCategoryChange}
162
+ onSelectSticker={onSelectSticker}
163
+ />
164
+ </KContainer.View>
165
+ );
166
+ };
@@ -0,0 +1,26 @@
1
+ import { memo } from 'react';
2
+ import type { BaseLegendMessageProps } from '../types';
3
+ import { KContainer, KImage } from '@droppii/libs';
4
+ import { StyleSheet } from 'react-native';
5
+
6
+ const STICKER_SIZE = 120;
7
+ export const StickerMessage = memo(({ message }: BaseLegendMessageProps) => {
8
+ return (
9
+ <KContainer.View style={styles.message}>
10
+ <KImage.Base
11
+ uri={message?.stickerElem?.content || ''}
12
+ width={STICKER_SIZE}
13
+ height={STICKER_SIZE}
14
+ />
15
+ </KContainer.View>
16
+ );
17
+ });
18
+
19
+ const styles = StyleSheet.create({
20
+ message: {
21
+ width: STICKER_SIZE,
22
+ height: STICKER_SIZE,
23
+ justifyContent: 'center',
24
+ alignItems: 'center',
25
+ },
26
+ });
@@ -17,6 +17,9 @@ export interface ApiEndpoints {
17
17
  imageUpload: string;
18
18
  bot: string;
19
19
  uploadFcmToken: string;
20
+ stickerCategories: string;
21
+ stickersByCategory: string;
22
+ stickerHistory: string;
20
23
  }
21
24
 
22
25
  const DEFAULT_ENDPOINTS: ApiEndpoints = {
@@ -25,6 +28,9 @@ const DEFAULT_ENDPOINTS: ApiEndpoints = {
25
28
  imageUpload: '/uploader-service/v1/uploader/permanently',
26
29
  bot: '/chat-service/v1/app/bots/',
27
30
  uploadFcmToken: '/chat-service/third/fcm_update_token',
31
+ stickerCategories: '/chat-service/v1/app/sticker-categories',
32
+ stickersByCategory: '/chat-service/v1/app/sticker-categories',
33
+ stickerHistory: '/chat-service/v1/app/stickers/history',
28
34
  };
29
35
 
30
36
  let endpoints: ApiEndpoints = { ...DEFAULT_ENDPOINTS };
@@ -3,7 +3,7 @@ import OpenIMSDK, { SessionType } from '@droppii/openim-rn-client-sdk';
3
3
  import { useConversationStore } from '../../store/conversation';
4
4
  import { apiInstance } from '../../services/apis';
5
5
  import type { BaseResponse } from '../../types/auth';
6
- import type { DBotStatus } from '../../types/chat';
6
+ import type { DBotStatus, DConversationTarget } from '../../types/chat';
7
7
  import { conversationQueryKeys } from '../query-keys';
8
8
  import { ENDPOINTS } from '../../services/endpoints';
9
9
 
@@ -46,6 +46,25 @@ export const useGetStatusConversationBot = (botId?: string) => {
46
46
  refetchOnMount: 'always',
47
47
  });
48
48
  };
49
+ export const useGetConversationTarget = (conversationId?: string) =>
50
+ useQuery({
51
+ queryKey: conversationQueryKeys.conversationTarget(conversationId || ''),
52
+ queryFn: async () => {
53
+ try {
54
+ const res = await apiInstance?.get<BaseResponse<DConversationTarget>>(
55
+ ENDPOINTS.chatService.getConversationTarget(conversationId || '')
56
+ );
57
+ return res?.data?.data || undefined;
58
+ } catch {
59
+ return undefined; // fail-open: unresolved status => compose stays enabled
60
+ }
61
+ },
62
+ enabled: !!conversationId,
63
+ staleTime: 0,
64
+ gcTime: 0,
65
+ refetchOnMount: 'always',
66
+ });
67
+
49
68
  export const useGetOneConversation = ({
50
69
  targetUserId,
51
70
  targetGroupId,
@@ -12,7 +12,7 @@ export const useGetConversationFlowMainMenu = (conversationId?: string) => {
12
12
  const res = await apiInstance?.get<BaseResponse<InputButtonItem[]>>(
13
13
  ENDPOINTS.chatService.conversationFlowMainMenu(conversationId || '')
14
14
  );
15
- return res?.data?.data || undefined;
15
+ return res?.data?.data || ([] as InputButtonItem[]);
16
16
  },
17
17
  enabled: !!conversationId,
18
18
  });