@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.
- package/lib/module/components/StickerPicker/StickerCategoryTabs.js +86 -0
- package/lib/module/components/StickerPicker/StickerCategoryTabs.js.map +1 -0
- package/lib/module/components/StickerPicker/StickerPickerPlaceholder.js +18 -0
- package/lib/module/components/StickerPicker/StickerPickerPlaceholder.js.map +1 -0
- package/lib/module/components/StickerPicker/StickerPickerRowItem.js +35 -0
- package/lib/module/components/StickerPicker/StickerPickerRowItem.js.map +1 -0
- package/lib/module/components/StickerPicker/StickerSectionList.js +110 -0
- package/lib/module/components/StickerPicker/StickerSectionList.js.map +1 -0
- package/lib/module/components/StickerPicker/constants.js +14 -0
- package/lib/module/components/StickerPicker/constants.js.map +1 -0
- package/lib/module/components/StickerPicker/index.js +134 -0
- package/lib/module/components/StickerPicker/index.js.map +1 -0
- package/lib/module/components/messages/stickerMessage/index.js +28 -0
- package/lib/module/components/messages/stickerMessage/index.js.map +1 -0
- package/lib/module/config/api-endpoints.js +4 -1
- package/lib/module/config/api-endpoints.js.map +1 -1
- package/lib/module/hooks/conversation/useGetConversationDetail.js +15 -0
- package/lib/module/hooks/conversation/useGetConversationDetail.js.map +1 -1
- package/lib/module/hooks/flows/useGetConversationFlowMainMenu.js +1 -1
- package/lib/module/hooks/flows/useGetConversationFlowMainMenu.js.map +1 -1
- package/lib/module/hooks/message/useSendMessage.js +45 -18
- package/lib/module/hooks/message/useSendMessage.js.map +1 -1
- package/lib/module/hooks/query-keys.js +7 -0
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/hooks/stickers/useFetchStickerByCategory.js +12 -0
- package/lib/module/hooks/stickers/useFetchStickerByCategory.js.map +1 -0
- package/lib/module/hooks/stickers/useStickerCategories.js +14 -0
- package/lib/module/hooks/stickers/useStickerCategories.js.map +1 -0
- package/lib/module/hooks/stickers/useStickerHistory.js +16 -0
- package/lib/module/hooks/stickers/useStickerHistory.js.map +1 -0
- package/lib/module/hooks/useChatCompose.js +1 -18
- package/lib/module/hooks/useChatCompose.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatComposer.js +64 -16
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +6 -20
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js +15 -4
- package/lib/module/screens/chat-detail/hooks/useChatComposerAnimation.js.map +1 -1
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js +3 -0
- package/lib/module/screens/chat-detail/hooks/useChatComposerState.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +5 -6
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/services/attachmentOrchestrator.js +1 -1
- package/lib/module/services/attachmentOrchestrator.js.map +1 -1
- package/lib/module/services/endpoints.js +4 -1
- package/lib/module/services/endpoints.js.map +1 -1
- package/lib/module/store/conversation.js +1 -20
- package/lib/module/store/conversation.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +3 -1
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js +6 -0
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/types/sticker.js +2 -0
- package/lib/module/types/sticker.js.map +1 -0
- package/lib/module/utils/conversation.js +1 -24
- package/lib/module/utils/conversation.js.map +1 -1
- package/lib/module/utils/messageUtils.js +2 -0
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/typescript/src/components/StickerPicker/StickerCategoryTabs.d.ts +9 -0
- package/lib/typescript/src/components/StickerPicker/StickerCategoryTabs.d.ts.map +1 -0
- package/lib/typescript/src/components/StickerPicker/StickerPickerPlaceholder.d.ts +2 -0
- package/lib/typescript/src/components/StickerPicker/StickerPickerPlaceholder.d.ts.map +1 -0
- package/lib/typescript/src/components/StickerPicker/StickerPickerRowItem.d.ts +8 -0
- package/lib/typescript/src/components/StickerPicker/StickerPickerRowItem.d.ts.map +1 -0
- package/lib/typescript/src/components/StickerPicker/StickerSectionList.d.ts +15 -0
- package/lib/typescript/src/components/StickerPicker/StickerSectionList.d.ts.map +1 -0
- package/lib/typescript/src/components/StickerPicker/constants.d.ts +11 -0
- package/lib/typescript/src/components/StickerPicker/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/StickerPicker/index.d.ts +7 -0
- package/lib/typescript/src/components/StickerPicker/index.d.ts.map +1 -0
- package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +3 -0
- package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -0
- package/lib/typescript/src/config/api-endpoints.d.ts +3 -0
- package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
- package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts +2 -1
- package/lib/typescript/src/hooks/conversation/useGetConversationDetail.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts +5 -3
- package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/src/hooks/query-keys.d.ts +7 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- package/lib/typescript/src/hooks/stickers/useFetchStickerByCategory.d.ts +3 -0
- package/lib/typescript/src/hooks/stickers/useFetchStickerByCategory.d.ts.map +1 -0
- package/lib/typescript/src/hooks/stickers/useStickerCategories.d.ts +3 -0
- package/lib/typescript/src/hooks/stickers/useStickerCategories.d.ts.map +1 -0
- package/lib/typescript/src/hooks/stickers/useStickerHistory.d.ts +3 -0
- package/lib/typescript/src/hooks/stickers/useStickerHistory.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useChatCompose.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +2 -2
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerAnimation.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts +3 -0
- package/lib/typescript/src/screens/chat-detail/hooks/useChatComposerState.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/services/endpoints.d.ts +4 -1
- package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
- package/lib/typescript/src/store/conversation.d.ts +0 -3
- package/lib/typescript/src/store/conversation.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +12 -19
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/types/sticker.d.ts +21 -0
- package/lib/typescript/src/types/sticker.d.ts.map +1 -0
- package/lib/typescript/src/utils/conversation.d.ts +1 -2
- package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/StickerPicker/StickerCategoryTabs.tsx +103 -0
- package/src/components/StickerPicker/StickerPickerPlaceholder.tsx +16 -0
- package/src/components/StickerPicker/StickerPickerRowItem.tsx +43 -0
- package/src/components/StickerPicker/StickerSectionList.tsx +141 -0
- package/src/components/StickerPicker/constants.ts +20 -0
- package/src/components/StickerPicker/index.tsx +166 -0
- package/src/components/messages/stickerMessage/index.tsx +26 -0
- package/src/config/api-endpoints.ts +6 -0
- package/src/hooks/conversation/useGetConversationDetail.ts +20 -1
- package/src/hooks/flows/useGetConversationFlowMainMenu.ts +1 -1
- package/src/hooks/message/useSendMessage.ts +49 -27
- package/src/hooks/query-keys.ts +10 -0
- package/src/hooks/stickers/useFetchStickerByCategory.ts +15 -0
- package/src/hooks/stickers/useStickerCategories.ts +17 -0
- package/src/hooks/stickers/useStickerHistory.ts +19 -0
- package/src/hooks/useChatCompose.ts +0 -19
- package/src/screens/chat-detail/ChatComposer.tsx +106 -19
- package/src/screens/chat-detail/ChatDetail.tsx +5 -19
- package/src/screens/chat-detail/hooks/useChatComposerAnimation.ts +21 -5
- package/src/screens/chat-detail/hooks/useChatComposerState.ts +7 -0
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +4 -5
- package/src/services/attachmentOrchestrator.ts +1 -1
- package/src/services/endpoints.ts +6 -1
- package/src/store/conversation.ts +0 -24
- package/src/translation/resources/i18n.ts +3 -0
- package/src/types/chat.ts +13 -21
- package/src/types/sticker.ts +23 -0
- package/src/utils/conversation.ts +0 -28
- 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 ||
|
|
15
|
+
return res?.data?.data || ([] as InputButtonItem[]);
|
|
16
16
|
},
|
|
17
17
|
enabled: !!conversationId,
|
|
18
18
|
});
|