@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.
Files changed (121) hide show
  1. package/lib/module/components/flows/inputButtons/index.js +11 -6
  2. package/lib/module/components/flows/inputButtons/index.js.map +1 -1
  3. package/lib/module/components/messages/linkMessage/index.js +4 -2
  4. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  5. package/lib/module/components/messages/quotedMessage/index.js +3 -2
  6. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  7. package/lib/module/components/messages/styles.js +6 -1
  8. package/lib/module/components/messages/styles.js.map +1 -1
  9. package/lib/module/components/messages/textMessage/index.js +3 -2
  10. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  11. package/lib/module/components/messages/types.js.map +1 -1
  12. package/lib/module/context/ChatContext.js +2 -3
  13. package/lib/module/context/ChatContext.js.map +1 -1
  14. package/lib/module/context/ChatDetailContext.js +5 -3
  15. package/lib/module/context/ChatDetailContext.js.map +1 -1
  16. package/lib/module/hooks/flows/useFlow.js +1 -1
  17. package/lib/module/hooks/flows/useFlow.js.map +1 -1
  18. package/lib/module/hooks/message/useSearchMessage.js +39 -14
  19. package/lib/module/hooks/message/useSearchMessage.js.map +1 -1
  20. package/lib/module/screens/SearchConversation/SearchAllResult.js +25 -10
  21. package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
  22. package/lib/module/screens/SearchConversation/SearchEmpty.js +22 -3
  23. package/lib/module/screens/SearchConversation/SearchEmpty.js.map +1 -1
  24. package/lib/module/screens/SearchConversation/SearchMessagesResult.js +23 -7
  25. package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
  26. package/lib/module/screens/SearchConversation/SearchResultItem.js +53 -12
  27. package/lib/module/screens/SearchConversation/SearchResultItem.js.map +1 -1
  28. package/lib/module/screens/SearchMessage/SearchMessageResult.js +24 -7
  29. package/lib/module/screens/SearchMessage/SearchMessageResult.js.map +1 -1
  30. package/lib/module/screens/SearchMessage/SearchResultItem.js +31 -3
  31. package/lib/module/screens/SearchMessage/SearchResultItem.js.map +1 -1
  32. package/lib/module/screens/chat-detail/ChatListLegend.js +26 -17
  33. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  34. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +4 -2
  35. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  36. package/lib/module/store/message.js +3 -0
  37. package/lib/module/store/message.js.map +1 -1
  38. package/lib/module/translation/resources/i18n.js +6 -2
  39. package/lib/module/translation/resources/i18n.js.map +1 -1
  40. package/lib/module/types/chat.js.map +1 -1
  41. package/lib/module/types/events.js +1 -0
  42. package/lib/module/types/events.js.map +1 -1
  43. package/lib/module/utils/flows.js +4 -4
  44. package/lib/module/utils/flows.js.map +1 -1
  45. package/lib/module/utils/highlightSearch.js +38 -0
  46. package/lib/module/utils/highlightSearch.js.map +1 -0
  47. package/lib/module/utils/messageUtils.js +5 -1
  48. package/lib/module/utils/messageUtils.js.map +1 -1
  49. package/lib/module/utils/toastFlashMessage.js +1 -1
  50. package/lib/module/utils/toastFlashMessage.js.map +1 -1
  51. package/lib/module/utils/toastFlashMessageConfig.js +1 -1
  52. package/lib/module/utils/toastFlashMessageConfig.js.map +1 -1
  53. package/lib/typescript/src/components/flows/inputButtons/index.d.ts +1 -1
  54. package/lib/typescript/src/components/flows/inputButtons/index.d.ts.map +1 -1
  55. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
  56. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  57. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/messages/styles.d.ts +5 -0
  59. package/lib/typescript/src/components/messages/styles.d.ts.map +1 -1
  60. package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
  61. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  62. package/lib/typescript/src/components/messages/types.d.ts +1 -0
  63. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  64. package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
  65. package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -1
  66. package/lib/typescript/src/hooks/message/useSearchMessage.d.ts.map +1 -1
  67. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
  68. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts +7 -1
  69. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts.map +1 -1
  70. package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
  71. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts +1 -0
  72. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts.map +1 -1
  73. package/lib/typescript/src/screens/SearchMessage/SearchMessageResult.d.ts.map +1 -1
  74. package/lib/typescript/src/screens/SearchMessage/SearchResultItem.d.ts +1 -0
  75. package/lib/typescript/src/screens/SearchMessage/SearchResultItem.d.ts.map +1 -1
  76. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  77. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
  78. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  79. package/lib/typescript/src/store/message.d.ts.map +1 -1
  80. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  81. package/lib/typescript/src/types/chat.d.ts +1 -1
  82. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  83. package/lib/typescript/src/types/events.d.ts +5 -1
  84. package/lib/typescript/src/types/events.d.ts.map +1 -1
  85. package/lib/typescript/src/types/flows.d.ts +4 -0
  86. package/lib/typescript/src/types/flows.d.ts.map +1 -1
  87. package/lib/typescript/src/utils/flows.d.ts +1 -1
  88. package/lib/typescript/src/utils/flows.d.ts.map +1 -1
  89. package/lib/typescript/src/utils/highlightSearch.d.ts +7 -0
  90. package/lib/typescript/src/utils/highlightSearch.d.ts.map +1 -0
  91. package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
  92. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  93. package/package.json +1 -1
  94. package/src/components/flows/inputButtons/index.tsx +20 -12
  95. package/src/components/messages/linkMessage/index.tsx +3 -0
  96. package/src/components/messages/quotedMessage/index.tsx +2 -1
  97. package/src/components/messages/styles.ts +6 -1
  98. package/src/components/messages/textMessage/index.tsx +7 -1
  99. package/src/components/messages/types.ts +1 -0
  100. package/src/context/ChatContext.tsx +2 -2
  101. package/src/context/ChatDetailContext.tsx +23 -10
  102. package/src/hooks/flows/useFlow.ts +5 -1
  103. package/src/hooks/message/useSearchMessage.ts +46 -15
  104. package/src/screens/SearchConversation/SearchAllResult.tsx +24 -10
  105. package/src/screens/SearchConversation/SearchEmpty.tsx +38 -14
  106. package/src/screens/SearchConversation/SearchMessagesResult.tsx +39 -18
  107. package/src/screens/SearchConversation/SearchResultItem.tsx +72 -13
  108. package/src/screens/SearchMessage/SearchMessageResult.tsx +47 -20
  109. package/src/screens/SearchMessage/SearchResultItem.tsx +37 -5
  110. package/src/screens/chat-detail/ChatListLegend.tsx +48 -21
  111. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +3 -0
  112. package/src/store/message.ts +2 -0
  113. package/src/translation/resources/i18n.ts +6 -2
  114. package/src/types/chat.ts +5 -1
  115. package/src/types/events.ts +5 -0
  116. package/src/types/flows.ts +4 -0
  117. package/src/utils/flows.ts +5 -6
  118. package/src/utils/highlightSearch.ts +48 -0
  119. package/src/utils/messageUtils.ts +16 -7
  120. package/src/utils/toastFlashMessage.ts +1 -1
  121. 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
- export const SearchEmpty = () => {
7
- return (
8
- <KContainer.View style={styles.container}>
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 { dataFlatten, hasNextPage, fetchNextPage, isFetchingNextPage } =
19
- useSearchMessage({
20
- query: searchTerm,
21
- applicationType,
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
- <FlatList
54
- data={dataFlatten}
55
- renderItem={renderItem}
56
- keyExtractor={keyExtractor}
57
- contentContainerStyle={styles.container}
58
- showsVerticalScrollIndicator={false}
59
- onEndReached={onEndReached}
60
- ItemSeparatorComponent={ItemSeparatorComponent}
61
- ListEmptyComponent={<SearchEmpty />}
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 { getLastMessageText } from '../../utils/messageUtils';
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 lastMessageText = getLastMessageText({
28
- message: item?.chatLog,
36
+ const senderName = getSenderName(
37
+ item?.chatLog,
29
38
  currentUserId,
30
- conversation: item?.conversation,
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
- <KLabel.Text typo="TextMdMedium" numberOfLines={1} flex>
50
- {item?.conversation?.showName}
51
- </KLabel.Text>
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
- <KLabel.Text typo="TextNmNormal" numberOfLines={1} flex>
57
- {lastMessageText}
58
- </KLabel.Text>
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 { dataFlatten, hasNextPage, fetchNextPage, isFetchingNextPage } =
19
- useSearchMessage({
20
- query: searchTerm,
21
- applicationType,
22
- conversationId,
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 <SearchResultItem item={item} onPressItem={onPressItem} />;
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
- <FlatList
49
- data={dataFlatten}
50
- renderItem={renderItem}
51
- keyExtractor={keyExtractor}
52
- contentContainerStyle={styles.container}
53
- showsVerticalScrollIndicator={false}
54
- onEndReached={onEndReached}
55
- ItemSeparatorComponent={ItemSeparatorComponent}
56
- ListEmptyComponent={<SearchEmpty />}
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
- <KLabel.Text typo="TextNmNormal" numberOfLines={1} flex>
55
- {lastMessageText}
56
- </KLabel.Text>
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 { getFlowInputButtonsNode } from '../../utils/flows';
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 { IRunConversationFlowPayload } from '../../types/flows';
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 isLastIncoming =
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 flowInputButtonsNode = null;
162
- if (isLastIncoming && isBotConversation) {
163
- flowInputButtonsNode = getFlowInputButtonsNode(
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
- getFlowInputButtonsNode(conversationFlow?.flowCurrentStep)
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?.scrollToIndex({
431
- index,
432
- animated: true,
433
- viewPosition: 0.5,
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
 
@@ -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
- 'Không tìm thấy kết quả nào\nHãy thử lại với cách viết hoặc kết hợp cụm từ khác phù hợp hơn.',
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: (step?: IConversationFlowStep) => void;
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;
@@ -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
+ }
@@ -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 {