@droppii-org/chat-mobile 0.2.63 → 0.2.65

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 (124) 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/ChatDetailContext.js +29 -6
  13. package/lib/module/context/ChatDetailContext.js.map +1 -1
  14. package/lib/module/core/useChatListener.js +21 -10
  15. package/lib/module/core/useChatListener.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/hooks/message/useSendMessage.js +13 -3
  21. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  22. package/lib/module/screens/SearchConversation/SearchAllResult.js +25 -10
  23. package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
  24. package/lib/module/screens/SearchConversation/SearchEmpty.js +22 -3
  25. package/lib/module/screens/SearchConversation/SearchEmpty.js.map +1 -1
  26. package/lib/module/screens/SearchConversation/SearchMessagesResult.js +23 -7
  27. package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
  28. package/lib/module/screens/SearchConversation/SearchResultItem.js +53 -12
  29. package/lib/module/screens/SearchConversation/SearchResultItem.js.map +1 -1
  30. package/lib/module/screens/SearchMessage/SearchMessageResult.js +24 -7
  31. package/lib/module/screens/SearchMessage/SearchMessageResult.js.map +1 -1
  32. package/lib/module/screens/SearchMessage/SearchResultItem.js +31 -3
  33. package/lib/module/screens/SearchMessage/SearchResultItem.js.map +1 -1
  34. package/lib/module/screens/chat-detail/ChatComposer.js +12 -1
  35. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  36. package/lib/module/screens/chat-detail/ChatListLegend.js +26 -17
  37. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  38. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +4 -2
  39. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  40. package/lib/module/store/message.js +3 -0
  41. package/lib/module/store/message.js.map +1 -1
  42. package/lib/module/translation/resources/i18n.js +7 -2
  43. package/lib/module/translation/resources/i18n.js.map +1 -1
  44. package/lib/module/types/chat.js.map +1 -1
  45. package/lib/module/types/events.js +2 -0
  46. package/lib/module/types/events.js.map +1 -1
  47. package/lib/module/utils/flows.js +4 -4
  48. package/lib/module/utils/flows.js.map +1 -1
  49. package/lib/module/utils/highlightSearch.js +38 -0
  50. package/lib/module/utils/highlightSearch.js.map +1 -0
  51. package/lib/module/utils/messageUtils.js +5 -1
  52. package/lib/module/utils/messageUtils.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/core/useChatListener.d.ts.map +1 -1
  66. package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -1
  67. package/lib/typescript/src/hooks/message/useSearchMessage.d.ts.map +1 -1
  68. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  69. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
  70. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts +7 -1
  71. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts.map +1 -1
  72. package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
  73. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts +1 -0
  74. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts.map +1 -1
  75. package/lib/typescript/src/screens/SearchMessage/SearchMessageResult.d.ts.map +1 -1
  76. package/lib/typescript/src/screens/SearchMessage/SearchResultItem.d.ts +1 -0
  77. package/lib/typescript/src/screens/SearchMessage/SearchResultItem.d.ts.map +1 -1
  78. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  79. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  80. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +2 -1
  81. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  82. package/lib/typescript/src/store/message.d.ts.map +1 -1
  83. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  84. package/lib/typescript/src/types/chat.d.ts +3 -2
  85. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  86. package/lib/typescript/src/types/events.d.ts +10 -1
  87. package/lib/typescript/src/types/events.d.ts.map +1 -1
  88. package/lib/typescript/src/types/flows.d.ts +4 -0
  89. package/lib/typescript/src/types/flows.d.ts.map +1 -1
  90. package/lib/typescript/src/utils/flows.d.ts +1 -1
  91. package/lib/typescript/src/utils/flows.d.ts.map +1 -1
  92. package/lib/typescript/src/utils/highlightSearch.d.ts +7 -0
  93. package/lib/typescript/src/utils/highlightSearch.d.ts.map +1 -0
  94. package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
  95. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  96. package/package.json +2 -2
  97. package/src/components/flows/inputButtons/index.tsx +20 -12
  98. package/src/components/messages/linkMessage/index.tsx +3 -0
  99. package/src/components/messages/quotedMessage/index.tsx +2 -1
  100. package/src/components/messages/styles.ts +6 -1
  101. package/src/components/messages/textMessage/index.tsx +7 -1
  102. package/src/components/messages/types.ts +1 -0
  103. package/src/context/ChatDetailContext.tsx +57 -11
  104. package/src/core/useChatListener.ts +53 -17
  105. package/src/hooks/flows/useFlow.ts +5 -1
  106. package/src/hooks/message/useSearchMessage.ts +46 -15
  107. package/src/hooks/message/useSendMessage.ts +21 -2
  108. package/src/screens/SearchConversation/SearchAllResult.tsx +24 -10
  109. package/src/screens/SearchConversation/SearchEmpty.tsx +38 -14
  110. package/src/screens/SearchConversation/SearchMessagesResult.tsx +39 -18
  111. package/src/screens/SearchConversation/SearchResultItem.tsx +72 -13
  112. package/src/screens/SearchMessage/SearchMessageResult.tsx +47 -20
  113. package/src/screens/SearchMessage/SearchResultItem.tsx +37 -5
  114. package/src/screens/chat-detail/ChatComposer.tsx +17 -1
  115. package/src/screens/chat-detail/ChatListLegend.tsx +48 -21
  116. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +3 -0
  117. package/src/store/message.ts +2 -0
  118. package/src/translation/resources/i18n.ts +7 -2
  119. package/src/types/chat.ts +7 -1
  120. package/src/types/events.ts +12 -0
  121. package/src/types/flows.ts +4 -0
  122. package/src/utils/flows.ts +5 -6
  123. package/src/utils/highlightSearch.ts +48 -0
  124. package/src/utils/messageUtils.ts +16 -7
@@ -13,7 +13,7 @@ import { useSearchMessage } from '../../hooks/message/useSearchMessage';
13
13
  import { SearchConversationTabResultType } from './types';
14
14
  import type { ISearchMessageItem } from '../../types/chat';
15
15
  import { trans } from '../../translation';
16
- import { FlatList, StyleSheet } from 'react-native';
16
+ import { ActivityIndicator, FlatList, StyleSheet } from 'react-native';
17
17
  import { SearchEmpty } from './SearchEmpty';
18
18
  import SearchResultItem from './SearchResultItem';
19
19
 
@@ -34,7 +34,7 @@ const PAGE_SIZE = 5;
34
34
  const SearchAllResult = memo(
35
35
  ({ searchTerm, handleChangeTab, onPressItem }: SearchAllResultProps) => {
36
36
  const { applicationType } = useChatContext();
37
- const { dataFlatten: messagesData } = useSearchMessage({
37
+ const { dataFlatten: messagesData, isFetching } = useSearchMessage({
38
38
  query: searchTerm,
39
39
  applicationType,
40
40
  pageSize: PAGE_SIZE,
@@ -90,6 +90,7 @@ const SearchAllResult = memo(
90
90
  item={childItem}
91
91
  tabResultType={item.type}
92
92
  onPressItem={onPressItem}
93
+ searchTerm={searchTerm}
93
94
  />
94
95
  </KContainer.View>
95
96
  );
@@ -107,7 +108,7 @@ const SearchAllResult = memo(
107
108
  </KContainer.View>
108
109
  );
109
110
  },
110
- [onChangeTab, onPressItem]
111
+ [onChangeTab, onPressItem, searchTerm]
111
112
  );
112
113
 
113
114
  const keyExtractor = useCallback((item: ISearchAllResultItem) => {
@@ -119,13 +120,26 @@ const SearchAllResult = memo(
119
120
  }, []);
120
121
 
121
122
  return (
122
- <FlatList
123
- data={data}
124
- renderItem={renderSessionItem}
125
- keyExtractor={keyExtractor}
126
- ListEmptyComponent={<SearchEmpty />}
127
- ItemSeparatorComponent={ItemSeparatorComponent}
128
- />
123
+ <KContainer.View flex>
124
+ {isFetching ? (
125
+ <KContainer.View flex center>
126
+ <ActivityIndicator color={KColors.primary.normal} size={'small'} />
127
+ </KContainer.View>
128
+ ) : (
129
+ <FlatList
130
+ data={data}
131
+ renderItem={renderSessionItem}
132
+ keyExtractor={keyExtractor}
133
+ ListEmptyComponent={
134
+ <SearchEmpty
135
+ searchTerm={searchTerm}
136
+ searchType={SearchConversationTabResultType.All}
137
+ />
138
+ }
139
+ ItemSeparatorComponent={ItemSeparatorComponent}
140
+ />
141
+ )}
142
+ </KContainer.View>
129
143
  );
130
144
  }
131
145
  );
@@ -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>
@@ -63,6 +63,8 @@ import { ChatCurrentQuotedMessage } from './ChatCurrentQuotedMessage';
63
63
  import { useSendMessage } from '../../hooks/message/useSendMessage';
64
64
  import { openBotMenuSheet } from './BotMenuSheet';
65
65
  import { UIUtils } from '../../utils/UIUtils';
66
+ import { GroupPermission, SessionType } from '@droppii/openim-rn-client-sdk';
67
+ import { useConversationStore } from '../../store';
66
68
 
67
69
  interface ChatComposerInternalProps {
68
70
  isEmptyMessage?: boolean;
@@ -95,6 +97,7 @@ export const ChatComposer = memo(
95
97
  const { mutateAsync: closeSupportSession } = useCloseSupportSession();
96
98
  const { sendMessage } = useSendMessage();
97
99
  const compose = useChatCompose();
100
+ const groupPermissions = useChatDetailContext().groupPermissions;
98
101
 
99
102
  const {
100
103
  activePanelType,
@@ -129,13 +132,26 @@ export const ChatComposer = memo(
129
132
  });
130
133
 
131
134
  const handleSend = useCallback(async () => {
135
+ const currentConversation = useConversationStore
136
+ .getState()
137
+ .getCurrentConversation();
138
+ if (
139
+ currentConversation?.conversationType === SessionType.Group &&
140
+ !groupPermissions?.includes(GroupPermission.SendMessage)
141
+ ) {
142
+ UIUtils.toast.open({
143
+ title: trans('chat:no_send_permission'),
144
+ theme: 'info',
145
+ });
146
+ return;
147
+ }
132
148
  if (compose.isUploading || !compose.canSend) return;
133
149
  try {
134
150
  await compose.send();
135
151
  } catch (error) {
136
152
  if (__DEV__) console.error('[ChatComposer] Failed to send:', error);
137
153
  }
138
- }, [compose]);
154
+ }, [compose, groupPermissions]);
139
155
 
140
156
  const handleInputFocus = useCallback(() => {
141
157
  setIsInputFocused(true);