@droppii-org/chat-mobile 0.2.62 → 0.2.64
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/flows/inputButtons/index.js +11 -6
- package/lib/module/components/flows/inputButtons/index.js.map +1 -1
- package/lib/module/components/messages/linkMessage/index.js +4 -2
- package/lib/module/components/messages/linkMessage/index.js.map +1 -1
- package/lib/module/components/messages/quotedMessage/index.js +3 -2
- package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
- package/lib/module/components/messages/styles.js +6 -1
- package/lib/module/components/messages/styles.js.map +1 -1
- package/lib/module/components/messages/textMessage/index.js +3 -2
- package/lib/module/components/messages/textMessage/index.js.map +1 -1
- package/lib/module/components/messages/types.js.map +1 -1
- package/lib/module/context/ChatContext.js +2 -3
- package/lib/module/context/ChatContext.js.map +1 -1
- package/lib/module/context/ChatDetailContext.js +5 -3
- package/lib/module/context/ChatDetailContext.js.map +1 -1
- package/lib/module/hooks/flows/useFlow.js +1 -1
- package/lib/module/hooks/flows/useFlow.js.map +1 -1
- package/lib/module/hooks/message/useSearchMessage.js +39 -14
- package/lib/module/hooks/message/useSearchMessage.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchAllResult.js +25 -10
- package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchEmpty.js +22 -3
- package/lib/module/screens/SearchConversation/SearchEmpty.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchMessagesResult.js +23 -7
- package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
- package/lib/module/screens/SearchConversation/SearchResultItem.js +53 -12
- package/lib/module/screens/SearchConversation/SearchResultItem.js.map +1 -1
- package/lib/module/screens/SearchMessage/SearchMessageResult.js +24 -7
- package/lib/module/screens/SearchMessage/SearchMessageResult.js.map +1 -1
- package/lib/module/screens/SearchMessage/SearchResultItem.js +31 -3
- package/lib/module/screens/SearchMessage/SearchResultItem.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +26 -17
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +4 -2
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/store/message.js +3 -0
- package/lib/module/store/message.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +6 -2
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/types/events.js +1 -0
- package/lib/module/types/events.js.map +1 -1
- package/lib/module/utils/flows.js +4 -4
- package/lib/module/utils/flows.js.map +1 -1
- package/lib/module/utils/highlightSearch.js +38 -0
- package/lib/module/utils/highlightSearch.js.map +1 -0
- package/lib/module/utils/messageUtils.js +5 -1
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/module/utils/toastFlashMessage.js +1 -1
- package/lib/module/utils/toastFlashMessage.js.map +1 -1
- package/lib/module/utils/toastFlashMessageConfig.js +1 -1
- package/lib/module/utils/toastFlashMessageConfig.js.map +1 -1
- package/lib/typescript/src/components/flows/inputButtons/index.d.ts +1 -1
- package/lib/typescript/src/components/flows/inputButtons/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/styles.d.ts +5 -0
- package/lib/typescript/src/components/messages/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
- package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/components/messages/types.d.ts +1 -0
- package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
- package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -1
- package/lib/typescript/src/hooks/message/useSearchMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts +7 -1
- package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts +1 -0
- package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchMessage/SearchMessageResult.d.ts.map +1 -1
- package/lib/typescript/src/screens/SearchMessage/SearchResultItem.d.ts +1 -0
- package/lib/typescript/src/screens/SearchMessage/SearchResultItem.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/store/message.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 +1 -1
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +5 -1
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/types/flows.d.ts +4 -0
- package/lib/typescript/src/types/flows.d.ts.map +1 -1
- package/lib/typescript/src/utils/flows.d.ts +1 -1
- package/lib/typescript/src/utils/flows.d.ts.map +1 -1
- package/lib/typescript/src/utils/highlightSearch.d.ts +7 -0
- package/lib/typescript/src/utils/highlightSearch.d.ts.map +1 -0
- package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/flows/inputButtons/index.tsx +20 -12
- package/src/components/messages/linkMessage/index.tsx +3 -0
- package/src/components/messages/quotedMessage/index.tsx +2 -1
- package/src/components/messages/styles.ts +6 -1
- package/src/components/messages/textMessage/index.tsx +7 -1
- package/src/components/messages/types.ts +1 -0
- package/src/context/ChatContext.tsx +2 -2
- package/src/context/ChatDetailContext.tsx +23 -10
- package/src/hooks/flows/useFlow.ts +5 -1
- package/src/hooks/message/useSearchMessage.ts +46 -15
- package/src/screens/SearchConversation/SearchAllResult.tsx +24 -10
- package/src/screens/SearchConversation/SearchEmpty.tsx +38 -14
- package/src/screens/SearchConversation/SearchMessagesResult.tsx +39 -18
- package/src/screens/SearchConversation/SearchResultItem.tsx +72 -13
- package/src/screens/SearchMessage/SearchMessageResult.tsx +47 -20
- package/src/screens/SearchMessage/SearchResultItem.tsx +37 -5
- package/src/screens/chat-detail/ChatListLegend.tsx +48 -21
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +3 -0
- package/src/store/message.ts +2 -0
- package/src/translation/resources/i18n.ts +6 -2
- package/src/types/chat.ts +5 -1
- package/src/types/events.ts +5 -0
- package/src/types/flows.ts +4 -0
- package/src/utils/flows.ts +5 -6
- package/src/utils/highlightSearch.ts +48 -0
- package/src/utils/messageUtils.ts +16 -7
- package/src/utils/toastFlashMessage.ts +1 -1
- package/src/utils/toastFlashMessageConfig.tsx +1 -1
|
@@ -2,23 +2,47 @@ import { KContainer, KImage, KLabel, KSpacingValue } from '@droppii/libs';
|
|
|
2
2
|
import { StyleSheet } from 'react-native';
|
|
3
3
|
import Images from '../../assets/images';
|
|
4
4
|
import { trans } from '../../translation';
|
|
5
|
+
import { memo, useMemo } from 'react';
|
|
6
|
+
import { isEmpty } from 'lodash';
|
|
7
|
+
import { SearchConversationTabResultType } from './types';
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<KImage.Base
|
|
10
|
-
uri={Images.SEARCH_EMPTY}
|
|
11
|
-
width={100}
|
|
12
|
-
height={100}
|
|
13
|
-
resizeMode="contain"
|
|
14
|
-
/>
|
|
15
|
-
<KLabel.Text typo="TextMdNormal" textAlign="center">
|
|
16
|
-
{trans('search:empty_description')}
|
|
17
|
-
</KLabel.Text>
|
|
18
|
-
</KContainer.View>
|
|
19
|
-
);
|
|
9
|
+
type SearchEmptyProps = {
|
|
10
|
+
searchTerm?: string;
|
|
11
|
+
searchType?: SearchConversationTabResultType;
|
|
20
12
|
};
|
|
21
13
|
|
|
14
|
+
export const SearchEmpty = memo(
|
|
15
|
+
({ searchTerm, searchType }: SearchEmptyProps) => {
|
|
16
|
+
const emptyHint = useMemo(() => {
|
|
17
|
+
if (!isEmpty(searchTerm?.trim())) {
|
|
18
|
+
return 'search:empty_description';
|
|
19
|
+
}
|
|
20
|
+
switch (searchType) {
|
|
21
|
+
case SearchConversationTabResultType.Messages:
|
|
22
|
+
return 'search_message:empty_hint';
|
|
23
|
+
case SearchConversationTabResultType.Conversations:
|
|
24
|
+
return 'search_conversation:empty_hint';
|
|
25
|
+
default:
|
|
26
|
+
return 'search:empty_hint';
|
|
27
|
+
}
|
|
28
|
+
}, [searchTerm, searchType]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<KContainer.View style={styles.container}>
|
|
32
|
+
<KImage.Base
|
|
33
|
+
uri={Images.SEARCH_EMPTY}
|
|
34
|
+
width={100}
|
|
35
|
+
height={100}
|
|
36
|
+
resizeMode="contain"
|
|
37
|
+
/>
|
|
38
|
+
<KLabel.Text typo="TextMdNormal" textAlign="center">
|
|
39
|
+
{trans(emptyHint)}
|
|
40
|
+
</KLabel.Text>
|
|
41
|
+
</KContainer.View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
22
46
|
const styles = StyleSheet.create({
|
|
23
47
|
container: {
|
|
24
48
|
paddingHorizontal: KSpacingValue['2rem'],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { KContainer, KDivider, KSpacingValue } from '@droppii/libs';
|
|
1
|
+
import { KColors, KContainer, KDivider, KSpacingValue } from '@droppii/libs';
|
|
2
2
|
import { memo, useCallback } from 'react';
|
|
3
3
|
import { useSearchMessage } from '../../hooks/message/useSearchMessage';
|
|
4
4
|
import { useChatContext } from '../../context';
|
|
5
|
-
import { FlatList, StyleSheet } from 'react-native';
|
|
5
|
+
import { FlatList, StyleSheet, ActivityIndicator } from 'react-native';
|
|
6
6
|
import type { ISearchMessageItem } from '../../types/chat';
|
|
7
7
|
import SearchResultItem from './SearchResultItem';
|
|
8
8
|
import { SearchConversationTabResultType } from './types';
|
|
@@ -15,11 +15,18 @@ interface ISearchMessagesResultProps {
|
|
|
15
15
|
const SearchMessagesResult = memo(
|
|
16
16
|
({ searchTerm, onPressItem }: ISearchMessagesResultProps) => {
|
|
17
17
|
const { applicationType } = useChatContext();
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const {
|
|
19
|
+
dataFlatten,
|
|
20
|
+
hasNextPage,
|
|
21
|
+
fetchNextPage,
|
|
22
|
+
isFetchingNextPage,
|
|
23
|
+
isFetching,
|
|
24
|
+
isRefetching,
|
|
25
|
+
refetch,
|
|
26
|
+
} = useSearchMessage({
|
|
27
|
+
query: searchTerm,
|
|
28
|
+
applicationType,
|
|
29
|
+
});
|
|
23
30
|
|
|
24
31
|
const renderItem = useCallback(
|
|
25
32
|
({ item }: { item: ISearchMessageItem }) => {
|
|
@@ -28,10 +35,11 @@ const SearchMessagesResult = memo(
|
|
|
28
35
|
item={item}
|
|
29
36
|
tabResultType={SearchConversationTabResultType.Messages}
|
|
30
37
|
onPressItem={onPressItem}
|
|
38
|
+
searchTerm={searchTerm}
|
|
31
39
|
/>
|
|
32
40
|
);
|
|
33
41
|
},
|
|
34
|
-
[onPressItem]
|
|
42
|
+
[onPressItem, searchTerm]
|
|
35
43
|
);
|
|
36
44
|
|
|
37
45
|
const keyExtractor = useCallback((item: ISearchMessageItem) => {
|
|
@@ -50,16 +58,29 @@ const SearchMessagesResult = memo(
|
|
|
50
58
|
|
|
51
59
|
return (
|
|
52
60
|
<KContainer.View flex>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
{isFetching && !isFetchingNextPage && !isRefetching ? (
|
|
62
|
+
<KContainer.View flex center>
|
|
63
|
+
<ActivityIndicator color={KColors.primary.normal} size={'small'} />
|
|
64
|
+
</KContainer.View>
|
|
65
|
+
) : (
|
|
66
|
+
<FlatList
|
|
67
|
+
data={dataFlatten}
|
|
68
|
+
renderItem={renderItem}
|
|
69
|
+
keyExtractor={keyExtractor}
|
|
70
|
+
contentContainerStyle={styles.container}
|
|
71
|
+
showsVerticalScrollIndicator={false}
|
|
72
|
+
onEndReached={onEndReached}
|
|
73
|
+
ItemSeparatorComponent={ItemSeparatorComponent}
|
|
74
|
+
ListEmptyComponent={
|
|
75
|
+
<SearchEmpty
|
|
76
|
+
searchTerm={searchTerm}
|
|
77
|
+
searchType={SearchConversationTabResultType.Messages}
|
|
78
|
+
/>
|
|
79
|
+
}
|
|
80
|
+
refreshing={isFetching}
|
|
81
|
+
onRefresh={refetch}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
63
84
|
</KContainer.View>
|
|
64
85
|
);
|
|
65
86
|
}
|
|
@@ -5,32 +5,65 @@ import {
|
|
|
5
5
|
KLabel,
|
|
6
6
|
KSpacingValue,
|
|
7
7
|
} from '@droppii/libs';
|
|
8
|
-
import { memo, useCallback } from 'react';
|
|
8
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
9
9
|
import type { ISearchMessageItem } from '../../types/chat';
|
|
10
10
|
import { StyleSheet } from 'react-native';
|
|
11
11
|
import { SearchConversationTabResultType } from './types';
|
|
12
12
|
import { AvatarSection, NamePrefixIcon } from '../../components/ThreadCard';
|
|
13
|
-
import {
|
|
13
|
+
import { getMessagePreview, getSenderName } from '../../utils/messageUtils';
|
|
14
14
|
import { useUserStore } from '../../store';
|
|
15
15
|
import { formatTimestamp } from '../../utils/dateTimeUtils';
|
|
16
|
+
import {
|
|
17
|
+
highlightSearch,
|
|
18
|
+
type HighlightResult,
|
|
19
|
+
} from '../../utils/highlightSearch';
|
|
20
|
+
import isEmpty from 'lodash/isEmpty';
|
|
21
|
+
import { MessageType } from '@droppii/openim-rn-client-sdk';
|
|
22
|
+
import { UIUtils } from '../../utils/UIUtils';
|
|
23
|
+
import { trans } from '../../translation';
|
|
16
24
|
|
|
17
25
|
interface ISearchResultItemProps {
|
|
18
26
|
item: ISearchMessageItem;
|
|
19
27
|
tabResultType: SearchConversationTabResultType;
|
|
20
28
|
onPressItem?: (item: ISearchMessageItem) => void;
|
|
29
|
+
searchTerm?: string;
|
|
21
30
|
}
|
|
22
31
|
const SearchResultItem = memo((props: ISearchResultItemProps) => {
|
|
23
|
-
const { item, tabResultType, onPressItem } = props || {};
|
|
32
|
+
const { item, tabResultType, onPressItem, searchTerm } = props || {};
|
|
24
33
|
const currentUserId = useUserStore((s) => s.user?.userID);
|
|
25
34
|
|
|
26
35
|
const timestamp = formatTimestamp(item?.chatLog?.sendTime ?? null);
|
|
27
|
-
const
|
|
28
|
-
|
|
36
|
+
const senderName = getSenderName(
|
|
37
|
+
item?.chatLog,
|
|
29
38
|
currentUserId,
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
item?.conversation
|
|
40
|
+
);
|
|
41
|
+
const lastMessageText = getMessagePreview(item?.chatLog);
|
|
42
|
+
|
|
43
|
+
const highlightResult: HighlightResult | null = useMemo(() => {
|
|
44
|
+
if (isEmpty(searchTerm?.trim())) return null;
|
|
45
|
+
if (tabResultType === SearchConversationTabResultType.Conversations) {
|
|
46
|
+
return highlightSearch(
|
|
47
|
+
item?.conversation?.showName || '',
|
|
48
|
+
searchTerm || ''
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return highlightSearch(lastMessageText, searchTerm || '');
|
|
52
|
+
}, [
|
|
53
|
+
lastMessageText,
|
|
54
|
+
searchTerm,
|
|
55
|
+
tabResultType,
|
|
56
|
+
item?.conversation?.showName,
|
|
57
|
+
]);
|
|
32
58
|
|
|
33
59
|
const handlePressItem = useCallback(() => {
|
|
60
|
+
const isRevoked = item?.chatLog?.contentType === MessageType.RevokeMessage;
|
|
61
|
+
if (isRevoked) {
|
|
62
|
+
UIUtils.toast.open({
|
|
63
|
+
title: trans('search_message:revoked_message'),
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
34
67
|
onPressItem?.({ conversation: item?.conversation, chatLog: item?.chatLog });
|
|
35
68
|
}, [onPressItem, item?.conversation, item?.chatLog]);
|
|
36
69
|
|
|
@@ -46,16 +79,42 @@ const SearchResultItem = memo((props: ISearchResultItemProps) => {
|
|
|
46
79
|
<KContainer.View style={styles.contentContainer}>
|
|
47
80
|
<KContainer.View style={styles.messageContainer}>
|
|
48
81
|
<NamePrefixIcon peerType={item?.conversation?.peerType} />
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
{!!highlightResult &&
|
|
83
|
+
tabResultType === SearchConversationTabResultType.Conversations ? (
|
|
84
|
+
<KLabel.Text typo="TextMdMedium" numberOfLines={1} flex>
|
|
85
|
+
{highlightResult?.before || ''}
|
|
86
|
+
<KLabel.Text typo="TextMdMedium" color={KColors.primary.normal}>
|
|
87
|
+
{highlightResult?.match || ''}
|
|
88
|
+
</KLabel.Text>
|
|
89
|
+
<KLabel.Text typo="TextMdMedium">
|
|
90
|
+
{highlightResult?.after || ''}
|
|
91
|
+
</KLabel.Text>
|
|
92
|
+
</KLabel.Text>
|
|
93
|
+
) : (
|
|
94
|
+
<KLabel.Text typo="TextMdMedium" numberOfLines={1} flex>
|
|
95
|
+
{item?.conversation?.showName || ''}
|
|
96
|
+
</KLabel.Text>
|
|
97
|
+
)}
|
|
52
98
|
<KLabel.Text typo="TextXsNormal" color={KColors.gray.light}>
|
|
53
99
|
{timestamp}
|
|
54
100
|
</KLabel.Text>
|
|
55
101
|
</KContainer.View>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
102
|
+
{!!highlightResult &&
|
|
103
|
+
tabResultType === SearchConversationTabResultType.Messages ? (
|
|
104
|
+
<KLabel.Text typo="TextNmNormal" numberOfLines={1} flex>
|
|
105
|
+
{`${senderName}: ${highlightResult?.before || ''}`}
|
|
106
|
+
<KLabel.Text typo="TextNmNormal" color={KColors.primary.normal}>
|
|
107
|
+
{highlightResult?.match || ''}
|
|
108
|
+
</KLabel.Text>
|
|
109
|
+
<KLabel.Text typo="TextNmNormal">
|
|
110
|
+
{highlightResult?.after || ''}
|
|
111
|
+
</KLabel.Text>
|
|
112
|
+
</KLabel.Text>
|
|
113
|
+
) : (
|
|
114
|
+
<KLabel.Text typo="TextNmNormal" numberOfLines={1} flex>
|
|
115
|
+
{`${senderName}: ${lastMessageText}`}
|
|
116
|
+
</KLabel.Text>
|
|
117
|
+
)}
|
|
59
118
|
</KContainer.View>
|
|
60
119
|
</KContainer.View>
|
|
61
120
|
<KContainer.VisibleView
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { KContainer, KDivider, KSpacingValue } from '@droppii/libs';
|
|
1
|
+
import { KColors, KContainer, KDivider, KSpacingValue } from '@droppii/libs';
|
|
2
2
|
import { memo, useCallback } from 'react';
|
|
3
3
|
import { useSearchMessage } from '../../hooks/message/useSearchMessage';
|
|
4
4
|
import { useChatContext } from '../../context';
|
|
5
|
-
import { FlatList, StyleSheet } from 'react-native';
|
|
5
|
+
import { ActivityIndicator, FlatList, StyleSheet } from 'react-native';
|
|
6
6
|
import type { ISearchMessageItem } from '../../types/chat';
|
|
7
7
|
import { SearchEmpty } from '../SearchConversation/SearchEmpty';
|
|
8
8
|
import SearchResultItem from './SearchResultItem';
|
|
9
|
+
import { SearchConversationTabResultType } from '../SearchConversation/types';
|
|
9
10
|
|
|
10
11
|
interface ISearchMessagesResultProps {
|
|
11
12
|
searchTerm: string;
|
|
@@ -15,18 +16,31 @@ interface ISearchMessagesResultProps {
|
|
|
15
16
|
const SearchMessagesResult = memo(
|
|
16
17
|
({ searchTerm, onPressItem, conversationId }: ISearchMessagesResultProps) => {
|
|
17
18
|
const { applicationType } = useChatContext();
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const {
|
|
20
|
+
dataFlatten,
|
|
21
|
+
hasNextPage,
|
|
22
|
+
fetchNextPage,
|
|
23
|
+
isFetchingNextPage,
|
|
24
|
+
isFetching,
|
|
25
|
+
isRefetching,
|
|
26
|
+
refetch,
|
|
27
|
+
} = useSearchMessage({
|
|
28
|
+
query: searchTerm,
|
|
29
|
+
applicationType,
|
|
30
|
+
conversationId,
|
|
31
|
+
});
|
|
24
32
|
|
|
25
33
|
const renderItem = useCallback(
|
|
26
34
|
({ item }: { item: ISearchMessageItem }) => {
|
|
27
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
<SearchResultItem
|
|
37
|
+
item={item}
|
|
38
|
+
onPressItem={onPressItem}
|
|
39
|
+
searchTerm={searchTerm}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
28
42
|
},
|
|
29
|
-
[onPressItem]
|
|
43
|
+
[onPressItem, searchTerm]
|
|
30
44
|
);
|
|
31
45
|
|
|
32
46
|
const keyExtractor = useCallback((item: ISearchMessageItem) => {
|
|
@@ -45,16 +59,29 @@ const SearchMessagesResult = memo(
|
|
|
45
59
|
|
|
46
60
|
return (
|
|
47
61
|
<KContainer.View flex>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
{isFetching && !isFetchingNextPage && !isRefetching ? (
|
|
63
|
+
<KContainer.View flex center>
|
|
64
|
+
<ActivityIndicator color={KColors.primary.normal} size={'small'} />
|
|
65
|
+
</KContainer.View>
|
|
66
|
+
) : (
|
|
67
|
+
<FlatList
|
|
68
|
+
data={dataFlatten}
|
|
69
|
+
renderItem={renderItem}
|
|
70
|
+
keyExtractor={keyExtractor}
|
|
71
|
+
contentContainerStyle={styles.container}
|
|
72
|
+
showsVerticalScrollIndicator={false}
|
|
73
|
+
onEndReached={onEndReached}
|
|
74
|
+
ItemSeparatorComponent={ItemSeparatorComponent}
|
|
75
|
+
ListEmptyComponent={
|
|
76
|
+
<SearchEmpty
|
|
77
|
+
searchTerm={searchTerm}
|
|
78
|
+
searchType={SearchConversationTabResultType.Messages}
|
|
79
|
+
/>
|
|
80
|
+
}
|
|
81
|
+
refreshing={isFetching}
|
|
82
|
+
onRefresh={refetch}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
58
85
|
</KContainer.View>
|
|
59
86
|
);
|
|
60
87
|
}
|
|
@@ -5,14 +5,22 @@ import { StyleSheet } from 'react-native';
|
|
|
5
5
|
import { AvatarSection, NamePrefixIcon } from '../../components/ThreadCard';
|
|
6
6
|
import { getMessagePreview } from '../../utils/messageUtils';
|
|
7
7
|
import { formatTimestamp } from '../../utils/dateTimeUtils';
|
|
8
|
-
import { PeerType } from '@droppii/openim-rn-client-sdk';
|
|
8
|
+
import { MessageType, PeerType } from '@droppii/openim-rn-client-sdk';
|
|
9
|
+
import {
|
|
10
|
+
highlightSearch,
|
|
11
|
+
type HighlightResult,
|
|
12
|
+
} from '../../utils/highlightSearch';
|
|
13
|
+
import isEmpty from 'lodash/isEmpty';
|
|
14
|
+
import { UIUtils } from '../../utils/UIUtils';
|
|
15
|
+
import { trans } from '../../translation';
|
|
9
16
|
|
|
10
17
|
interface ISearchResultItemProps {
|
|
11
18
|
item: ISearchMessageItem;
|
|
12
19
|
onPressItem?: (item: ISearchMessageItem) => void;
|
|
20
|
+
searchTerm?: string;
|
|
13
21
|
}
|
|
14
22
|
const SearchResultItem = memo((props: ISearchResultItemProps) => {
|
|
15
|
-
const { item, onPressItem } = props || {};
|
|
23
|
+
const { item, onPressItem, searchTerm } = props || {};
|
|
16
24
|
|
|
17
25
|
const isBotConversation = useMemo(() => {
|
|
18
26
|
return item?.conversation?.peerType === PeerType.Bot;
|
|
@@ -28,7 +36,19 @@ const SearchResultItem = memo((props: ISearchResultItemProps) => {
|
|
|
28
36
|
? item?.conversation?.showName
|
|
29
37
|
: item?.chatLog?.senderNickname;
|
|
30
38
|
|
|
39
|
+
const highlightResult: HighlightResult | null = useMemo(() => {
|
|
40
|
+
if (isEmpty(searchTerm?.trim())) return null;
|
|
41
|
+
return highlightSearch(lastMessageText, searchTerm || '');
|
|
42
|
+
}, [lastMessageText, searchTerm]);
|
|
43
|
+
|
|
31
44
|
const handlePressItem = useCallback(() => {
|
|
45
|
+
const isRevoked = item?.chatLog?.contentType === MessageType.RevokeMessage;
|
|
46
|
+
if (isRevoked) {
|
|
47
|
+
UIUtils.toast.open({
|
|
48
|
+
title: trans('search_message:revoked_message'),
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
32
52
|
onPressItem?.({ conversation: item?.conversation, chatLog: item?.chatLog });
|
|
33
53
|
}, [onPressItem, item?.conversation, item?.chatLog]);
|
|
34
54
|
|
|
@@ -51,9 +71,21 @@ const SearchResultItem = memo((props: ISearchResultItemProps) => {
|
|
|
51
71
|
{timestamp}
|
|
52
72
|
</KLabel.Text>
|
|
53
73
|
</KContainer.View>
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
|
|
74
|
+
{highlightResult ? (
|
|
75
|
+
<KLabel.Text typo="TextNmNormal" numberOfLines={1} flex>
|
|
76
|
+
{highlightResult?.before || ''}
|
|
77
|
+
<KLabel.Text typo="TextNmNormal" color={KColors.primary.normal}>
|
|
78
|
+
{highlightResult?.match || ''}
|
|
79
|
+
</KLabel.Text>
|
|
80
|
+
<KLabel.Text typo="TextNmNormal">
|
|
81
|
+
{highlightResult?.after || ''}
|
|
82
|
+
</KLabel.Text>
|
|
83
|
+
</KLabel.Text>
|
|
84
|
+
) : (
|
|
85
|
+
<KLabel.Text typo="TextNmNormal" numberOfLines={1} flex>
|
|
86
|
+
{lastMessageText}
|
|
87
|
+
</KLabel.Text>
|
|
88
|
+
)}
|
|
57
89
|
</KContainer.View>
|
|
58
90
|
</KContainer.View>
|
|
59
91
|
</KContainer.Touchable>
|
|
@@ -29,10 +29,13 @@ import { LegendChatMessage } from './legend/LegendChatMessage';
|
|
|
29
29
|
import { LegendChatDay } from './legend/LegendChatDay';
|
|
30
30
|
import { LegendChatLoadEarlier } from './legend/LegendChatLoadEarlier';
|
|
31
31
|
import { useChatDetailContext } from '../../context/ChatDetailContext';
|
|
32
|
-
import {
|
|
32
|
+
import { getFlowInputButtonsNodes } from '../../utils/flows';
|
|
33
33
|
import { InputButtonFlow } from '../../components/flows/inputButtons';
|
|
34
34
|
import { useResetFlow, useRunFlow } from '../../hooks/flows/useFlow';
|
|
35
|
-
import type {
|
|
35
|
+
import type {
|
|
36
|
+
GroupChild,
|
|
37
|
+
IRunConversationFlowPayload,
|
|
38
|
+
} from '../../types/flows';
|
|
36
39
|
import { useSendMessage } from '../../hooks/message/useSendMessage';
|
|
37
40
|
import { ChatEvent, type ScrollToMessagePayload } from '../../types/events';
|
|
38
41
|
import { useMessageStore } from '../../store/message';
|
|
@@ -110,6 +113,10 @@ export const ChatListLegend = memo(
|
|
|
110
113
|
const scrollDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
111
114
|
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
112
115
|
const [showBadge, setShowBadge] = useState(false);
|
|
116
|
+
const [highlightedId, setHighlightedId] = useState<string | null>(null);
|
|
117
|
+
const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
118
|
+
null
|
|
119
|
+
);
|
|
113
120
|
|
|
114
121
|
const { applicationType } = useChatContext();
|
|
115
122
|
const searchMessages = useMessageStore((s) => s.searchMessages);
|
|
@@ -143,9 +150,9 @@ export const ChatListLegend = memo(
|
|
|
143
150
|
|
|
144
151
|
const message = item as MessageItem;
|
|
145
152
|
const isOutgoing = message.sendID === currentUserId;
|
|
146
|
-
const
|
|
147
|
-
!isOutgoing &&
|
|
153
|
+
const isLastMessage =
|
|
148
154
|
(message.clientMsgID || message.serverMsgID) === lastMessageId;
|
|
155
|
+
const isLastIncoming = !isOutgoing && isLastMessage;
|
|
149
156
|
const messageId =
|
|
150
157
|
message.clientMsgID ||
|
|
151
158
|
message.serverMsgID ||
|
|
@@ -158,9 +165,13 @@ export const ChatListLegend = memo(
|
|
|
158
165
|
|
|
159
166
|
const { dayStart, createdAt } = messageData;
|
|
160
167
|
|
|
161
|
-
let
|
|
162
|
-
if (
|
|
163
|
-
|
|
168
|
+
let flowInputButtonsNodes: GroupChild<'input-buttons'>[] = [];
|
|
169
|
+
if (
|
|
170
|
+
((isLastMessage && conversationFlow?.havingBotMessage === false) ||
|
|
171
|
+
isLastIncoming) &&
|
|
172
|
+
isBotConversation
|
|
173
|
+
) {
|
|
174
|
+
flowInputButtonsNodes = getFlowInputButtonsNodes(
|
|
164
175
|
conversationFlow?.flowCurrentStep
|
|
165
176
|
);
|
|
166
177
|
}
|
|
@@ -193,15 +204,16 @@ export const ChatListLegend = memo(
|
|
|
193
204
|
: undefined
|
|
194
205
|
}
|
|
195
206
|
customMessageItemFactory={customMessageItemFactory}
|
|
207
|
+
isHighlighted={highlightedId === messageId}
|
|
196
208
|
/>
|
|
197
|
-
{!!flowInputButtonsNode && (
|
|
198
|
-
<InputButtonFlow
|
|
199
|
-
nodeData={flowInputButtonsNode}
|
|
200
|
-
onResetFlow={onResetFlow}
|
|
201
|
-
onRunFlow={onRunFlow}
|
|
202
|
-
/>
|
|
203
|
-
)}
|
|
204
209
|
</KContainer.View>
|
|
210
|
+
{flowInputButtonsNodes && flowInputButtonsNodes.length > 0 && (
|
|
211
|
+
<InputButtonFlow
|
|
212
|
+
nodeData={flowInputButtonsNodes}
|
|
213
|
+
onResetFlow={onResetFlow}
|
|
214
|
+
onRunFlow={onRunFlow}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
205
217
|
</>
|
|
206
218
|
);
|
|
207
219
|
} catch (error) {
|
|
@@ -229,7 +241,9 @@ export const ChatListLegend = memo(
|
|
|
229
241
|
onResetFlow,
|
|
230
242
|
onRunFlow,
|
|
231
243
|
conversationFlow?.flowCurrentStep,
|
|
244
|
+
conversationFlow?.havingBotMessage,
|
|
232
245
|
retryMessage,
|
|
246
|
+
highlightedId,
|
|
233
247
|
]
|
|
234
248
|
);
|
|
235
249
|
|
|
@@ -320,10 +334,13 @@ export const ChatListLegend = memo(
|
|
|
320
334
|
|
|
321
335
|
const isOutgoing = message.sendID === currentUserId;
|
|
322
336
|
const isLastIncoming = !isOutgoing && messageId === lastMessageId;
|
|
337
|
+
const flowInputButtonsNodes = getFlowInputButtonsNodes(
|
|
338
|
+
conversationFlow?.flowCurrentStep
|
|
339
|
+
);
|
|
323
340
|
if (
|
|
324
341
|
isLastIncoming &&
|
|
325
342
|
isBotConversation &&
|
|
326
|
-
|
|
343
|
+
flowInputButtonsNodes.length > 0
|
|
327
344
|
) {
|
|
328
345
|
return undefined;
|
|
329
346
|
}
|
|
@@ -427,11 +444,21 @@ export const ChatListLegend = memo(
|
|
|
427
444
|
|
|
428
445
|
if (index >= 0) {
|
|
429
446
|
try {
|
|
430
|
-
listRef.current
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
listRef.current
|
|
448
|
+
?.scrollToIndex({
|
|
449
|
+
index,
|
|
450
|
+
animated: true,
|
|
451
|
+
viewPosition: 0.5,
|
|
452
|
+
})
|
|
453
|
+
.finally(() => {
|
|
454
|
+
if (highlightTimerRef.current)
|
|
455
|
+
clearTimeout(highlightTimerRef.current);
|
|
456
|
+
setHighlightedId(clientMessageId);
|
|
457
|
+
highlightTimerRef.current = setTimeout(
|
|
458
|
+
() => setHighlightedId(null),
|
|
459
|
+
5000
|
|
460
|
+
);
|
|
461
|
+
});
|
|
435
462
|
} catch {
|
|
436
463
|
// scrollToIndex throws if item not rendered yet — force refetch messages
|
|
437
464
|
searchMessages({
|
|
@@ -487,7 +514,7 @@ export const ChatListLegend = memo(
|
|
|
487
514
|
getFixedItemSize={getFixedItemSize}
|
|
488
515
|
keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
|
|
489
516
|
recycleItems={false}
|
|
490
|
-
extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`}
|
|
517
|
+
extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}-${highlightedId ?? ''}`}
|
|
491
518
|
alignItemsAtEnd
|
|
492
519
|
maintainVisibleContentPosition={maintainVisiblePositionConfig}
|
|
493
520
|
maintainScrollAtEnd={true}
|
|
@@ -35,6 +35,7 @@ interface LegendChatMessageProps {
|
|
|
35
35
|
onPress?: (message: MessageItem) => void;
|
|
36
36
|
onLongPress?: () => void;
|
|
37
37
|
customMessageItemFactory?: (m: MessageItem) => ReactNode;
|
|
38
|
+
isHighlighted?: boolean;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const messageComponentMap: Partial<
|
|
@@ -69,6 +70,7 @@ export const LegendChatMessage = memo(
|
|
|
69
70
|
onPress,
|
|
70
71
|
onLongPress,
|
|
71
72
|
customMessageItemFactory,
|
|
73
|
+
isHighlighted,
|
|
72
74
|
}: LegendChatMessageProps) => {
|
|
73
75
|
const handlePress = useCallback(() => {
|
|
74
76
|
onPress?.(message);
|
|
@@ -107,6 +109,7 @@ export const LegendChatMessage = memo(
|
|
|
107
109
|
createdAtTime={createdAtTime}
|
|
108
110
|
onMediaPress={hasMediaPress ? onMediaPress : undefined}
|
|
109
111
|
onPressUrl={hasUrlPress ? onPressUrl : undefined}
|
|
112
|
+
isHighlighted={isHighlighted}
|
|
110
113
|
/>
|
|
111
114
|
</LongPressWrapper>
|
|
112
115
|
|
package/src/store/message.ts
CHANGED
|
@@ -224,6 +224,8 @@ export const markMessageRevoked = (clientMsgID: string) => {
|
|
|
224
224
|
.getState()
|
|
225
225
|
.updateOneMessage({ clientMsgID, contentType: MessageType.RevokeMessage });
|
|
226
226
|
|
|
227
|
+
DeviceEventEmitter.emit(ChatEvent.MessageRevoked, { clientMsgID });
|
|
228
|
+
|
|
227
229
|
revokeQuotedReferences(clientMsgID);
|
|
228
230
|
};
|
|
229
231
|
|
|
@@ -122,9 +122,13 @@ const i18nKey: Record<string, string> = {
|
|
|
122
122
|
'all': 'Tất cả',
|
|
123
123
|
'contacts': 'Liên hệ',
|
|
124
124
|
'messages': 'Tin nhắn',
|
|
125
|
-
'search:empty_description':
|
|
126
|
-
|
|
125
|
+
'search:empty_description': 'Không tìm thấy kết quả nào',
|
|
126
|
+
'search:empty_hint':
|
|
127
|
+
'Bạn có thể tìm kiếm theo liên hệ hoặc nội dung tin nhắn',
|
|
128
|
+
'search_message:empty_hint': 'Bạn có thể tìm kiếm theo nội dung tin nhắn',
|
|
129
|
+
'search_conversation:empty_hint': 'Bạn có thể tìm kiếm theo liên hệ',
|
|
127
130
|
'view_all': 'Xem tất cả',
|
|
131
|
+
'search_message:revoked_message': 'Tin nhắn đã được thu hồi',
|
|
128
132
|
};
|
|
129
133
|
|
|
130
134
|
export default {
|
package/src/types/chat.ts
CHANGED
|
@@ -205,7 +205,11 @@ export interface MessageStore {
|
|
|
205
205
|
export interface ChatDetailContextType {
|
|
206
206
|
conversationFlow?: IConversationFlow;
|
|
207
207
|
isBotConversation: boolean;
|
|
208
|
-
setConverationFlowStep: (
|
|
208
|
+
setConverationFlowStep: (
|
|
209
|
+
step?: IConversationFlowStep,
|
|
210
|
+
isEnd?: boolean,
|
|
211
|
+
havingBotMessage?: boolean
|
|
212
|
+
) => void;
|
|
209
213
|
flowState?: DFlowState;
|
|
210
214
|
runButtonContext: (stringContext: DFlowContext) => Promise<boolean>;
|
|
211
215
|
conversation?: ConversationItem;
|
package/src/types/events.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export enum ChatEvent {
|
|
2
2
|
ScrollToMessage = 'chat:scroll_to_message',
|
|
3
3
|
ScrollToBottom = 'chat:scroll_to_bottom',
|
|
4
|
+
MessageRevoked = 'chat:message_revoked',
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export interface ScrollToMessagePayload {
|
|
7
8
|
clientMessageId: string;
|
|
8
9
|
}
|
|
10
|
+
|
|
11
|
+
export interface MessageRevokedPayload {
|
|
12
|
+
clientMsgID: string;
|
|
13
|
+
}
|
package/src/types/flows.ts
CHANGED
|
@@ -91,10 +91,14 @@ export interface IConversationFlow {
|
|
|
91
91
|
chatType: PeerType;
|
|
92
92
|
applicationType: DChatApplicationType;
|
|
93
93
|
flowCurrentStep: IConversationFlowStep | null;
|
|
94
|
+
havingBotMessage: boolean;
|
|
95
|
+
flowIsEnd: boolean;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export interface IRunConversationFlowResponse {
|
|
97
99
|
flowNextStep: IConversationFlowStep | null;
|
|
100
|
+
isEnd: boolean;
|
|
101
|
+
havingBotMessage: boolean;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
export interface IRunConversationFlowPayload {
|