@droppii-org/chat-mobile 0.2.79 → 0.2.81

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 (187) hide show
  1. package/lib/module/components/messages/BubbleTail.js +31 -0
  2. package/lib/module/components/messages/BubbleTail.js.map +1 -0
  3. package/lib/module/components/messages/BubbleTailRow.js +46 -0
  4. package/lib/module/components/messages/BubbleTailRow.js.map +1 -0
  5. package/lib/module/components/messages/bubbleTail.utils.js +19 -0
  6. package/lib/module/components/messages/bubbleTail.utils.js.map +1 -0
  7. package/lib/module/components/messages/fileMessage/index.js +72 -65
  8. package/lib/module/components/messages/fileMessage/index.js.map +1 -1
  9. package/lib/module/components/messages/imageMessage/index.js +23 -17
  10. package/lib/module/components/messages/imageMessage/index.js.map +1 -1
  11. package/lib/module/components/messages/linkMessage/index.js +11 -23
  12. package/lib/module/components/messages/linkMessage/index.js.map +1 -1
  13. package/lib/module/components/messages/mergedMessage/index.js +14 -7
  14. package/lib/module/components/messages/mergedMessage/index.js.map +1 -1
  15. package/lib/module/components/messages/quotedMessage/index.js +11 -23
  16. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  17. package/lib/module/components/messages/revokedMessage/index.js +14 -7
  18. package/lib/module/components/messages/revokedMessage/index.js.map +1 -1
  19. package/lib/module/components/messages/stickerMessage/index.js +13 -7
  20. package/lib/module/components/messages/stickerMessage/index.js.map +1 -1
  21. package/lib/module/components/messages/textMessage/index.js +11 -26
  22. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  23. package/lib/module/components/messages/types.js.map +1 -1
  24. package/lib/module/components/messages/videoMessage/index.js +16 -8
  25. package/lib/module/components/messages/videoMessage/index.js.map +1 -1
  26. package/lib/module/config/api-endpoints.js +2 -1
  27. package/lib/module/config/api-endpoints.js.map +1 -1
  28. package/lib/module/hooks/contacts/useSearchContacts.js +50 -0
  29. package/lib/module/hooks/contacts/useSearchContacts.js.map +1 -0
  30. package/lib/module/hooks/groups/useDismissGroup.js +2 -1
  31. package/lib/module/hooks/groups/useDismissGroup.js.map +1 -1
  32. package/lib/module/hooks/query-keys.js +4 -0
  33. package/lib/module/hooks/query-keys.js.map +1 -1
  34. package/lib/module/screens/SearchConversation/ContactResultItem.js +119 -0
  35. package/lib/module/screens/SearchConversation/ContactResultItem.js.map +1 -0
  36. package/lib/module/screens/SearchConversation/SearchAllResult.js +26 -20
  37. package/lib/module/screens/SearchConversation/SearchAllResult.js.map +1 -1
  38. package/lib/module/screens/SearchConversation/SearchCommunityResult.js +79 -0
  39. package/lib/module/screens/SearchConversation/SearchCommunityResult.js.map +1 -0
  40. package/lib/module/screens/SearchConversation/SearchContactsResult.js +117 -0
  41. package/lib/module/screens/SearchConversation/SearchContactsResult.js.map +1 -0
  42. package/lib/module/screens/SearchConversation/SearchEmpty.js +2 -0
  43. package/lib/module/screens/SearchConversation/SearchEmpty.js.map +1 -1
  44. package/lib/module/screens/SearchConversation/SearchMessagesResult.js +1 -8
  45. package/lib/module/screens/SearchConversation/SearchMessagesResult.js.map +1 -1
  46. package/lib/module/screens/SearchConversation/SearchResultItem.js +2 -0
  47. package/lib/module/screens/SearchConversation/SearchResultItem.js.map +1 -1
  48. package/lib/module/screens/SearchConversation/TabResult.js +24 -7
  49. package/lib/module/screens/SearchConversation/TabResult.js.map +1 -1
  50. package/lib/module/screens/SearchConversation/index.js +6 -2
  51. package/lib/module/screens/SearchConversation/index.js.map +1 -1
  52. package/lib/module/screens/SearchConversation/types.js +1 -0
  53. package/lib/module/screens/SearchConversation/types.js.map +1 -1
  54. package/lib/module/screens/chat-detail/ChatListLegend.js +71 -21
  55. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  56. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +8 -2
  57. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -1
  58. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +38 -13
  59. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  60. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js +11 -1
  61. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxWrapper.js.map +1 -1
  62. package/lib/module/services/endpoints.js +2 -1
  63. package/lib/module/services/endpoints.js.map +1 -1
  64. package/lib/module/translation/resources/i18n.js +6 -0
  65. package/lib/module/translation/resources/i18n.js.map +1 -1
  66. package/lib/module/types/chat.js +8 -0
  67. package/lib/module/types/chat.js.map +1 -1
  68. package/lib/module/utils/dateTimeUtils.js +10 -0
  69. package/lib/module/utils/dateTimeUtils.js.map +1 -1
  70. package/lib/module/utils/highlightSearch.js +6 -2
  71. package/lib/module/utils/highlightSearch.js.map +1 -1
  72. package/lib/module/utils/legendListMessage.js +86 -12
  73. package/lib/module/utils/legendListMessage.js.map +1 -1
  74. package/lib/module/utils/message.js +19 -1
  75. package/lib/module/utils/message.js.map +1 -1
  76. package/lib/module/utils/messageUtils.js +16 -2
  77. package/lib/module/utils/messageUtils.js.map +1 -1
  78. package/lib/typescript/src/components/messages/BubbleTail.d.ts +13 -0
  79. package/lib/typescript/src/components/messages/BubbleTail.d.ts.map +1 -0
  80. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts +25 -0
  81. package/lib/typescript/src/components/messages/BubbleTailRow.d.ts.map +1 -0
  82. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts +11 -0
  83. package/lib/typescript/src/components/messages/bubbleTail.utils.d.ts.map +1 -0
  84. package/lib/typescript/src/components/messages/fileMessage/index.d.ts +1 -1
  85. package/lib/typescript/src/components/messages/fileMessage/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/messages/imageMessage/index.d.ts +1 -1
  87. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  88. package/lib/typescript/src/components/messages/linkMessage/index.d.ts +1 -1
  89. package/lib/typescript/src/components/messages/linkMessage/index.d.ts.map +1 -1
  90. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts +1 -1
  91. package/lib/typescript/src/components/messages/mergedMessage/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts +1 -1
  94. package/lib/typescript/src/components/messages/revokedMessage/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +1 -1
  96. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/messages/textMessage/index.d.ts +1 -1
  98. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  99. package/lib/typescript/src/components/messages/types.d.ts +3 -0
  100. package/lib/typescript/src/components/messages/types.d.ts.map +1 -1
  101. package/lib/typescript/src/components/messages/videoMessage/index.d.ts +1 -1
  102. package/lib/typescript/src/components/messages/videoMessage/index.d.ts.map +1 -1
  103. package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
  104. package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
  105. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts +221 -0
  106. package/lib/typescript/src/hooks/contacts/useSearchContacts.d.ts.map +1 -0
  107. package/lib/typescript/src/hooks/groups/useDismissGroup.d.ts.map +1 -1
  108. package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
  109. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  110. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts +9 -0
  111. package/lib/typescript/src/screens/SearchConversation/ContactResultItem.d.ts.map +1 -0
  112. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts +3 -2
  113. package/lib/typescript/src/screens/SearchConversation/SearchAllResult.d.ts.map +1 -1
  114. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts +7 -0
  115. package/lib/typescript/src/screens/SearchConversation/SearchCommunityResult.d.ts.map +1 -0
  116. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts +8 -0
  117. package/lib/typescript/src/screens/SearchConversation/SearchContactsResult.d.ts.map +1 -0
  118. package/lib/typescript/src/screens/SearchConversation/SearchEmpty.d.ts.map +1 -1
  119. package/lib/typescript/src/screens/SearchConversation/SearchMessagesResult.d.ts.map +1 -1
  120. package/lib/typescript/src/screens/SearchConversation/SearchResultItem.d.ts.map +1 -1
  121. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts +4 -2
  122. package/lib/typescript/src/screens/SearchConversation/TabResult.d.ts.map +1 -1
  123. package/lib/typescript/src/screens/SearchConversation/index.d.ts +4 -2
  124. package/lib/typescript/src/screens/SearchConversation/index.d.ts.map +1 -1
  125. package/lib/typescript/src/screens/SearchConversation/types.d.ts +2 -1
  126. package/lib/typescript/src/screens/SearchConversation/types.d.ts.map +1 -1
  127. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  128. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -1
  129. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -1
  130. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +14 -1
  131. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  132. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.d.ts.map +1 -1
  133. package/lib/typescript/src/services/endpoints.d.ts +1 -0
  134. package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
  135. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  136. package/lib/typescript/src/types/chat.d.ts +27 -0
  137. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  138. package/lib/typescript/src/utils/dateTimeUtils.d.ts +2 -0
  139. package/lib/typescript/src/utils/dateTimeUtils.d.ts.map +1 -1
  140. package/lib/typescript/src/utils/highlightSearch.d.ts.map +1 -1
  141. package/lib/typescript/src/utils/legendListMessage.d.ts +19 -4
  142. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
  143. package/lib/typescript/src/utils/message.d.ts +17 -0
  144. package/lib/typescript/src/utils/message.d.ts.map +1 -1
  145. package/lib/typescript/src/utils/messageUtils.d.ts +1 -0
  146. package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
  147. package/package.json +3 -1
  148. package/src/build-ignore.d.ts +2 -0
  149. package/src/components/messages/BubbleTail.tsx +35 -0
  150. package/src/components/messages/BubbleTailRow.tsx +64 -0
  151. package/src/components/messages/bubbleTail.utils.ts +21 -0
  152. package/src/components/messages/fileMessage/index.tsx +90 -83
  153. package/src/components/messages/imageMessage/index.tsx +26 -23
  154. package/src/components/messages/linkMessage/index.tsx +6 -18
  155. package/src/components/messages/mergedMessage/index.tsx +27 -15
  156. package/src/components/messages/quotedMessage/index.tsx +7 -19
  157. package/src/components/messages/revokedMessage/index.tsx +23 -14
  158. package/src/components/messages/stickerMessage/index.tsx +16 -11
  159. package/src/components/messages/textMessage/index.tsx +6 -22
  160. package/src/components/messages/types.ts +3 -0
  161. package/src/components/messages/videoMessage/index.tsx +13 -10
  162. package/src/config/api-endpoints.ts +2 -0
  163. package/src/hooks/contacts/useSearchContacts.ts +66 -0
  164. package/src/hooks/groups/useDismissGroup.tsx +1 -0
  165. package/src/hooks/query-keys.ts +6 -0
  166. package/src/screens/SearchConversation/ContactResultItem.tsx +128 -0
  167. package/src/screens/SearchConversation/SearchAllResult.tsx +38 -24
  168. package/src/screens/SearchConversation/SearchCommunityResult.tsx +96 -0
  169. package/src/screens/SearchConversation/SearchContactsResult.tsx +155 -0
  170. package/src/screens/SearchConversation/SearchEmpty.tsx +2 -0
  171. package/src/screens/SearchConversation/SearchMessagesResult.tsx +1 -6
  172. package/src/screens/SearchConversation/SearchResultItem.tsx +6 -1
  173. package/src/screens/SearchConversation/TabResult.tsx +40 -5
  174. package/src/screens/SearchConversation/index.tsx +10 -2
  175. package/src/screens/SearchConversation/types.tsx +1 -0
  176. package/src/screens/chat-detail/ChatListLegend.tsx +97 -28
  177. package/src/screens/chat-detail/legend/LegendChatDay.tsx +25 -15
  178. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +62 -12
  179. package/src/screens/chat-detail/messageToolbox/MessageToolboxWrapper.tsx +22 -4
  180. package/src/services/endpoints.ts +1 -0
  181. package/src/translation/resources/i18n.ts +6 -0
  182. package/src/types/chat.ts +30 -0
  183. package/src/utils/dateTimeUtils.ts +15 -0
  184. package/src/utils/highlightSearch.ts +12 -2
  185. package/src/utils/legendListMessage.ts +123 -14
  186. package/src/utils/message.ts +19 -1
  187. package/src/utils/messageUtils.ts +23 -9
@@ -1,4 +1,4 @@
1
- import { KColors, KContainer, KDivider, KSpacingValue } from '@droppii/libs';
1
+ import { KColors, KContainer, 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';
@@ -52,10 +52,6 @@ const SearchMessagesResult = memo(
52
52
  }
53
53
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
54
54
 
55
- const ItemSeparatorComponent = useCallback(() => {
56
- return <KDivider size={'hairline'} vertical={false} />;
57
- }, []);
58
-
59
55
  return (
60
56
  <KContainer.View flex>
61
57
  {isFetching && !isFetchingNextPage && !isRefetching ? (
@@ -70,7 +66,6 @@ const SearchMessagesResult = memo(
70
66
  contentContainerStyle={styles.container}
71
67
  showsVerticalScrollIndicator={false}
72
68
  onEndReached={onEndReached}
73
- ItemSeparatorComponent={ItemSeparatorComponent}
74
69
  ListEmptyComponent={
75
70
  <SearchEmpty
76
71
  searchTerm={searchTerm}
@@ -68,7 +68,12 @@ const SearchResultItem = memo((props: ISearchResultItemProps) => {
68
68
  }, [onPressItem, item?.conversation, item?.chatLog]);
69
69
 
70
70
  return (
71
- <KContainer.Touchable style={styles.container} onPress={handlePressItem}>
71
+ <KContainer.Touchable
72
+ style={styles.container}
73
+ onPress={handlePressItem}
74
+ brBW={1}
75
+ brBC={KColors.border.light}
76
+ >
72
77
  <KContainer.View style={styles.leftContainer}>
73
78
  <AvatarSection
74
79
  avatar={item?.conversation?.faceURL ?? null}
@@ -12,19 +12,30 @@ import { SearchConversationTabResultType } from './types';
12
12
  import { trans } from '../../translation';
13
13
  import SearchMessagesResult from './SearchMessagesResult';
14
14
  import SearchAllResult from './SearchAllResult';
15
- import type { ISearchMessageItem } from '../../types/chat';
15
+ import SearchContactsResult from './SearchContactsResult';
16
+ import SearchCommunityResult from './SearchCommunityResult';
17
+ import type { IContactSearchItem, ISearchMessageItem } from '../../types/chat';
16
18
 
17
19
  type TabRoute = { key: string; title: string };
18
20
 
19
21
  interface ISearchConversationTabResultProps {
20
22
  searchTerm: string;
21
23
  onPressItem?: (item: ISearchMessageItem) => void;
24
+ onPressSearchContact?: (item: IContactSearchItem) => void;
25
+ onPressGroup?: (groupId: string) => void;
22
26
  }
23
27
 
24
28
  const TabRoutes: TabRoute[] = [
25
29
  { key: SearchConversationTabResultType.All, title: trans('all') },
26
- // { key: SearchConversationTabResultType.Conversations, title: trans('contacts') },
30
+ {
31
+ key: SearchConversationTabResultType.Conversations,
32
+ title: trans('contacts'),
33
+ },
27
34
  { key: SearchConversationTabResultType.Messages, title: trans('messages') },
35
+ {
36
+ key: SearchConversationTabResultType.Community,
37
+ title: trans('community'),
38
+ },
28
39
  ];
29
40
 
30
41
  interface CustomTabBarProps {
@@ -58,7 +69,12 @@ const CustomTabBar = memo(({ routes, index, onPress }: CustomTabBarProps) => (
58
69
  ));
59
70
 
60
71
  const SearchConversationTabResult = memo(
61
- ({ searchTerm, onPressItem }: ISearchConversationTabResultProps) => {
72
+ ({
73
+ searchTerm,
74
+ onPressItem,
75
+ onPressSearchContact,
76
+ onPressGroup,
77
+ }: ISearchConversationTabResultProps) => {
62
78
  const [index, setIndex] = useState(0);
63
79
  const [routes] = useState<TabRoute[]>(TabRoutes);
64
80
 
@@ -78,10 +94,16 @@ const SearchConversationTabResult = memo(
78
94
  searchTerm={searchTerm}
79
95
  handleChangeTab={handleChangeTab}
80
96
  onPressItem={onPressItem}
97
+ onPressSearchContact={onPressSearchContact}
81
98
  />
82
99
  );
83
100
  case SearchConversationTabResultType.Conversations:
84
- return null;
101
+ return (
102
+ <SearchContactsResult
103
+ searchTerm={searchTerm}
104
+ onPressSearchContact={onPressSearchContact}
105
+ />
106
+ );
85
107
  case SearchConversationTabResultType.Messages:
86
108
  return (
87
109
  <SearchMessagesResult
@@ -89,11 +111,24 @@ const SearchConversationTabResult = memo(
89
111
  onPressItem={onPressItem}
90
112
  />
91
113
  );
114
+ case SearchConversationTabResultType.Community:
115
+ return (
116
+ <SearchCommunityResult
117
+ searchTerm={searchTerm}
118
+ onPressGroup={onPressGroup}
119
+ />
120
+ );
92
121
  default:
93
122
  return null;
94
123
  }
95
124
  },
96
- [searchTerm, handleChangeTab, onPressItem]
125
+ [
126
+ searchTerm,
127
+ handleChangeTab,
128
+ onPressItem,
129
+ onPressSearchContact,
130
+ onPressGroup,
131
+ ]
97
132
  );
98
133
 
99
134
  const renderTabBar = useCallback(
@@ -5,13 +5,19 @@ import { trans } from '../../translation';
5
5
  import debounce from 'lodash/debounce';
6
6
  import type { TextInput } from 'react-native';
7
7
  import SearchConversationTabResult from './TabResult';
8
- import type { ISearchMessageItem } from '../../types/chat';
8
+ import type { IContactSearchItem, ISearchMessageItem } from '../../types/chat';
9
9
 
10
10
  interface SearchConversationScreenProps {
11
11
  onPressItem?: (item: ISearchMessageItem) => void;
12
+ onPressSearchContact?: (item: IContactSearchItem) => void;
13
+ onPressGroup?: (groupId: string) => void;
12
14
  }
13
15
  const SearchConversationScreen = memo(
14
- ({ onPressItem }: SearchConversationScreenProps) => {
16
+ ({
17
+ onPressItem,
18
+ onPressSearchContact,
19
+ onPressGroup,
20
+ }: SearchConversationScreenProps) => {
15
21
  const nav = useNavigation();
16
22
  const [inputValue, setInputValue] = useState('');
17
23
  const [searchTerm, setSearchTerm] = useState('');
@@ -62,6 +68,8 @@ const SearchConversationScreen = memo(
62
68
  <SearchConversationTabResult
63
69
  searchTerm={searchTerm}
64
70
  onPressItem={onPressItem}
71
+ onPressSearchContact={onPressSearchContact}
72
+ onPressGroup={onPressGroup}
65
73
  />
66
74
  </KContainer.Page>
67
75
  );
@@ -2,4 +2,5 @@ export enum SearchConversationTabResultType {
2
2
  All = 'all',
3
3
  Conversations = 'conversations',
4
4
  Messages = 'messages',
5
+ Community = 'community',
5
6
  }
@@ -32,7 +32,11 @@ import {
32
32
  KRadiusValue,
33
33
  KImage,
34
34
  } from '@droppii/libs';
35
- import { precomputeLegendListData } from '../../utils/legendListMessage';
35
+ import {
36
+ precomputeLegendListData,
37
+ type GroupPosition,
38
+ } from '../../utils/legendListMessage';
39
+ import { getMessageRenderId } from '../../utils/message';
36
40
  import type { ChatListProps } from './types';
37
41
  import { LegendChatMessage } from './legend/LegendChatMessage';
38
42
  import { LegendChatDay } from './legend/LegendChatDay';
@@ -50,7 +54,7 @@ import { ChatEvent, type ScrollToMessagePayload } from '../../types/events';
50
54
  import { useMessageStore } from '../../store/message';
51
55
  import { useChatContext } from '../../context';
52
56
  import { messageStyles } from './legend/messageTypes';
53
- import { MessageType } from '@droppii/openim-rn-client-sdk';
57
+ import { MessageType, PeerType } from '@droppii/openim-rn-client-sdk';
54
58
  import type { MessageItem } from '@droppii/openim-rn-client-sdk';
55
59
  import { useUserStore } from '../../store';
56
60
  import { ChatMessageAPI } from '../../services';
@@ -60,10 +64,19 @@ import ChatImageDimens, {
60
64
  CHAT_IMAGE_VIEW_SOURCE,
61
65
  } from '../../utils/chatImageDimens';
62
66
 
63
- // Row wrapper (`messageRowLeft`/`messageRowRight` in `components/messages/styles.ts`)
64
- // adds only a bottom margin no vertical padding so a fixed-size item's true
65
- // rendered height is `content height + this margin`.
66
- const ROW_MARGIN_BOTTOM = KSpacingValue['0.25rem'];
67
+ // Cluster spacing: a wide gap after the message that closes a cluster, a tight
68
+ // one between messages inside it. These override the flat margin on the row
69
+ // wrapper (`messageRowLeft`/`messageRowRight` in `components/messages/styles.ts`),
70
+ // which adds only a bottom margin and no vertical padding — so a fixed-size
71
+ // item's true rendered height stays `content height + this margin`, and
72
+ // getFixedItemSize can keep folding it in.
73
+ const ROW_MARGIN_CLUSTER_END = KSpacingValue['0.5rem'];
74
+ const ROW_MARGIN_IN_CLUSTER = KSpacingValue['0.25rem'];
75
+
76
+ const getRowMarginBottom = (position: GroupPosition): number =>
77
+ position === 'last' || position === 'solo'
78
+ ? ROW_MARGIN_CLUSTER_END
79
+ : ROW_MARGIN_IN_CLUSTER;
67
80
 
68
81
  // KeyboardAwareLegendList auto scroll when modal opened
69
82
  // TODO: Convert toolbox to absolute view
@@ -103,12 +116,23 @@ export const ChatListLegend = memo(
103
116
  const currentUserId = useUserStore((s) => s.user?.userID);
104
117
  const windowWidth = useWindowDimensions().width;
105
118
  // Precompute all message data in single pass (O(n))
119
+ const { isBotConversation, conversationFlow, conversation } =
120
+ useChatDetailContext();
121
+
122
+ // Avatar and sender name only appear in real group chats — a bot thread is
123
+ // a Group session too, but reads as a single counterpart.
124
+ const isGroupConversation =
125
+ conversation?.peerType === PeerType.Group && !isBotConversation;
126
+
106
127
  const messageDataMap = useMemo(
107
- () => precomputeLegendListData(messages),
108
- [messages]
128
+ () =>
129
+ precomputeLegendListData(messages, {
130
+ isBotConversation,
131
+ currentUserId,
132
+ }),
133
+ [messages, isBotConversation, currentUserId]
109
134
  );
110
135
 
111
- const { isBotConversation, conversationFlow } = useChatDetailContext();
112
136
  const { retryMessage } = useSendMessage();
113
137
 
114
138
  const lastMessageId = useMemo(() => {
@@ -163,17 +187,22 @@ export const ChatListLegend = memo(
163
187
  const isLastMessage =
164
188
  (message.clientMsgID || message.serverMsgID) === lastMessageId;
165
189
  const isLastIncoming = !isOutgoing && isLastMessage;
166
- const messageId =
167
- message.clientMsgID ||
168
- message.serverMsgID ||
169
- `temp-${message.sendTime || message.createTime}-${index}`;
190
+ const messageId = getMessageRenderId(message, index);
170
191
 
171
192
  const messageData = messageDataMap.get(messageId);
172
193
  if (!messageData) {
173
194
  return null;
174
195
  }
175
196
 
176
- const { dayStart, createdAt } = messageData;
197
+ const { dayStart, timeBreak, createdAt, groupPosition } = messageData;
198
+
199
+ const showSenderGutter = isGroupConversation && !isOutgoing;
200
+ const showName =
201
+ showSenderGutter &&
202
+ (groupPosition === 'first' || groupPosition === 'solo');
203
+ const showAvatar =
204
+ showSenderGutter &&
205
+ (groupPosition === 'last' || groupPosition === 'solo');
177
206
 
178
207
  let flowInputButtonsNodes: GroupChild<'input-buttons'>[] = [];
179
208
  if (
@@ -188,17 +217,20 @@ export const ChatListLegend = memo(
188
217
 
189
218
  return (
190
219
  <>
191
- {dayStart && <LegendChatDay createdAt={createdAt} />}
220
+ {(dayStart || timeBreak) && (
221
+ <LegendChatDay createdAt={createdAt} timeOnly={!dayStart} />
222
+ )}
192
223
  <KContainer.View
193
224
  ref={(node: View | null) => {
194
225
  if (node) rowRefsMap.current.set(messageId, node);
195
226
  else rowRefsMap.current.delete(messageId);
196
227
  }}
197
- style={
228
+ style={[
198
229
  isOutgoing
199
230
  ? messageStyles.messageRowRight
200
- : messageStyles.messageRowLeft
201
- }
231
+ : messageStyles.messageRowLeft,
232
+ { marginBottom: getRowMarginBottom(groupPosition) },
233
+ ]}
202
234
  collapsable={false}
203
235
  >
204
236
  <LegendChatMessage
@@ -215,6 +247,10 @@ export const ChatListLegend = memo(
215
247
  }
216
248
  customMessageItemFactory={customMessageItemFactory}
217
249
  isHighlighted={highlightedId === messageId}
250
+ showName={showName}
251
+ showAvatar={showAvatar}
252
+ showSenderGutter={showSenderGutter}
253
+ groupPosition={groupPosition}
218
254
  />
219
255
  </KContainer.View>
220
256
  {flowInputButtonsNodes && flowInputButtonsNodes.length > 0 && (
@@ -242,6 +278,7 @@ export const ChatListLegend = memo(
242
278
  currentUserId,
243
279
  lastMessageId,
244
280
  messageDataMap,
281
+ isGroupConversation,
245
282
  isBotConversation,
246
283
  onMediaPress,
247
284
  onPressUrl,
@@ -303,12 +340,22 @@ export const ChatListLegend = memo(
303
340
  const getFixedItemSize = useCallback(
304
341
  (item: MessageItem, index: number): number | undefined => {
305
342
  const message = item;
306
- const messageId =
307
- message.clientMsgID ||
308
- message.serverMsgID ||
309
- `temp-${message.sendTime || message.createTime}-${index}`;
343
+ const messageId = getMessageRenderId(message, index);
310
344
 
311
- if (messageDataMap.get(messageId)?.dayStart) return undefined;
345
+ const messageData = messageDataMap.get(messageId);
346
+ // Both separators add chrome above the row that this function does not
347
+ // account for, and a resolved size is never re-measured.
348
+ if (messageData?.dayStart || messageData?.timeBreak) return undefined;
349
+
350
+ // Incoming rows in a group chat carry an avatar column and a sender
351
+ // name whose height this function cannot predict — measure them.
352
+ if (isGroupConversation && message.sendID !== currentUserId) {
353
+ return undefined;
354
+ }
355
+
356
+ const rowMargin = getRowMarginBottom(
357
+ messageData?.groupPosition ?? 'solo'
358
+ );
312
359
 
313
360
  // Resolve the content-type candidate FIRST — for every type other
314
361
  // than these three the result is `undefined` regardless of anything
@@ -319,17 +366,16 @@ export const ChatListLegend = memo(
319
366
  switch (message.contentType) {
320
367
  case MessageType.PictureMessage:
321
368
  candidateSize =
322
- resolveImageDisplayDims(message, windowWidth).height +
323
- ROW_MARGIN_BOTTOM;
369
+ resolveImageDisplayDims(message, windowWidth).height + rowMargin;
324
370
  break;
325
371
  case MessageType.VideoMessage:
326
372
  candidateSize =
327
373
  ChatImageDimens.getDimensions(
328
374
  CHAT_IMAGE_VIEW_SOURCE.MessageListThumbnail
329
- ).height + ROW_MARGIN_BOTTOM;
375
+ ).height + rowMargin;
330
376
  break;
331
377
  case MessageType.StickerMessage:
332
- candidateSize = STICKER_SIZE + ROW_MARGIN_BOTTOM;
378
+ candidateSize = STICKER_SIZE + rowMargin;
333
379
  break;
334
380
  default:
335
381
  return undefined;
@@ -361,6 +407,7 @@ export const ChatListLegend = memo(
361
407
  messageDataMap,
362
408
  customMessageItemFactory,
363
409
  currentUserId,
410
+ isGroupConversation,
364
411
  lastMessageId,
365
412
  isBotConversation,
366
413
  conversationFlow?.flowCurrentStep,
@@ -368,6 +415,28 @@ export const ChatListLegend = memo(
368
415
  ]
369
416
  );
370
417
 
418
+ // LegendList memoises each rendered item on [itemKey, item, extraData], so
419
+ // an item whose own object is unchanged will not re-render. Cluster
420
+ // position depends on a message's NEIGHBOURS, which can change while the
421
+ // message itself does not — prepending older history is enough to turn a
422
+ // 'first' into a 'middle'. messageDataMap is memoised on exactly the inputs
423
+ // that can move those boundaries, so including it here is what keeps a
424
+ // stale tail from being left behind.
425
+ const listExtraData = useMemo(
426
+ () => ({
427
+ messageDataMap,
428
+ flowStepId: conversationFlow?.flowCurrentStep?.id ?? '',
429
+ lastMessageId,
430
+ highlightedId,
431
+ }),
432
+ [
433
+ messageDataMap,
434
+ conversationFlow?.flowCurrentStep?.id,
435
+ lastMessageId,
436
+ highlightedId,
437
+ ]
438
+ );
439
+
371
440
  const handleStartReached = useCallback(() => {
372
441
  if (__DEV__) {
373
442
  console.log('[LegendList] onStartReached triggered', {
@@ -525,7 +594,7 @@ export const ChatListLegend = memo(
525
594
  getFixedItemSize={getFixedItemSize}
526
595
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
527
596
  recycleItems={false}
528
- extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}-${highlightedId ?? ''}`}
597
+ extraData={listExtraData}
529
598
  alignItemsAtEnd
530
599
  maintainVisibleContentPosition={maintainVisiblePositionConfig}
531
600
  freeze={freeze}
@@ -46,25 +46,35 @@ const formatDayLabel = (createdAt: number): string | null => {
46
46
 
47
47
  interface LegendChatDayProps {
48
48
  createdAt: number;
49
+ /**
50
+ * Render just the clock time, without the day. Used where a cluster broke on
51
+ * elapsed time inside a single day — the date is already on screen above.
52
+ */
53
+ timeOnly?: boolean;
49
54
  }
50
55
 
51
- export const LegendChatDay = memo(({ createdAt }: LegendChatDayProps) => {
52
- const dateStr = useMemo(
53
- () => (createdAt == null ? null : formatDayLabel(createdAt)),
54
- [createdAt]
55
- );
56
+ export const LegendChatDay = memo(
57
+ ({ createdAt, timeOnly = false }: LegendChatDayProps) => {
58
+ const dateStr = useMemo(() => {
59
+ if (createdAt == null) return null;
60
+ if (!timeOnly) return formatDayLabel(createdAt);
56
61
 
57
- if (!dateStr) return null;
62
+ const date = new Date(createdAt);
63
+ return Number.isNaN(date.getTime()) ? null : formatTime(date);
64
+ }, [createdAt, timeOnly]);
58
65
 
59
- return (
60
- <KContainer.View center marginV="0.5rem">
61
- <KContainer.View>
62
- <KLabel.Text typo="TextXsNormal" color={CHAT_BUBBLE_COLORS.dayLabel}>
63
- {dateStr}
64
- </KLabel.Text>
66
+ if (!dateStr) return null;
67
+
68
+ return (
69
+ <KContainer.View center marginV="0.5rem">
70
+ <KContainer.View>
71
+ <KLabel.Text typo="TextXsNormal" color={CHAT_BUBBLE_COLORS.dayLabel}>
72
+ {dateStr}
73
+ </KLabel.Text>
74
+ </KContainer.View>
65
75
  </KContainer.View>
66
- </KContainer.View>
67
- );
68
- });
76
+ );
77
+ }
78
+ );
69
79
 
70
80
  LegendChatDay.displayName = 'LegendChatDay';
@@ -1,4 +1,10 @@
1
- import { KContainer, KImage, KColors, KSpacingValue } from '@droppii/libs';
1
+ import {
2
+ KContainer,
3
+ KImage,
4
+ KColors,
5
+ KLabel,
6
+ KSpacingValue,
7
+ } from '@droppii/libs';
2
8
  import { StyleSheet } from 'react-native';
3
9
  import { memo, useCallback, useMemo } from 'react';
4
10
  import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
@@ -20,7 +26,10 @@ import type { BaseLegendMessageProps } from '../../../components/messages/types'
20
26
  import { messageStyles } from './messageTypes';
21
27
  import QuotedMessage from '../../../components/messages/quotedMessage';
22
28
  import { StickerMessage } from '../../../components/messages/stickerMessage';
23
- import { GROUP_NOTIFICATION_TEXT } from '../../../utils/messageUtils';
29
+ import { GROUP_NOTIFICATION_TYPES } from '../../../utils/messageUtils';
30
+ import type { GroupPosition } from '../../../utils/legendListMessage';
31
+ import { Avatar } from '../../../components/Avatar';
32
+ import { TAIL_WIDTH } from '../../../components/messages/BubbleTail';
24
33
 
25
34
  type MessageProps = BaseLegendMessageProps & {
26
35
  onPressUrl?: (url: string) => void;
@@ -38,18 +47,20 @@ interface LegendChatMessageProps {
38
47
  onLongPress?: () => void;
39
48
  customMessageItemFactory?: (m: MessageItem) => ReactNode;
40
49
  isHighlighted?: boolean;
50
+ /** Sender name above the bubble — first message of an incoming cluster. */
51
+ showName?: boolean;
52
+ /** Sender avatar beside the bubble — last message of an incoming cluster. */
53
+ showAvatar?: boolean;
54
+ /**
55
+ * Reserve the avatar column. True for every incoming message in a group
56
+ * conversation, not just the one carrying the avatar, so that the middle of
57
+ * a cluster stays aligned with its last message.
58
+ */
59
+ showSenderGutter?: boolean;
60
+ /** Position within its same-sender cluster — drives the bubble tail. */
61
+ groupPosition?: GroupPosition;
41
62
  }
42
63
 
43
- // Notification-style messages (group created/renamed/member changes/etc.)
44
- // are system banners, not user-authored text — they render centered,
45
- // full-width, without a bubble, avatar/status chrome, or left/right
46
- // alignment (see SYSTEM_MESSAGE_TYPES below). Driven by the same lookup
47
- // messageUtils uses to build their preview/thread text, so adding a new
48
- // notification content type there is enough to cover both places.
49
- const GROUP_NOTIFICATION_TYPES = Object.keys(GROUP_NOTIFICATION_TEXT).map(
50
- Number
51
- ) as MessageType[];
52
-
53
64
  const messageComponentMap: Partial<
54
65
  Record<MessageType, ComponentType<MessageProps>>
55
66
  > = {
@@ -88,6 +99,10 @@ export const LegendChatMessage = memo(
88
99
  onLongPress,
89
100
  customMessageItemFactory,
90
101
  isHighlighted,
102
+ showName = false,
103
+ showAvatar = false,
104
+ showSenderGutter = false,
105
+ groupPosition,
91
106
  }: LegendChatMessageProps) => {
92
107
  const handlePress = useCallback(() => {
93
108
  onPress?.(message);
@@ -127,12 +142,34 @@ export const LegendChatMessage = memo(
127
142
 
128
143
  return (
129
144
  <KContainer.View style={messageStyles.wrapper} row alignItems>
145
+ {showSenderGutter && (
146
+ <KContainer.View style={styles.avatarGutter}>
147
+ {showAvatar && (
148
+ <Avatar
149
+ size="sm"
150
+ source={message.senderFaceUrl}
151
+ fullName={message.senderNickname}
152
+ />
153
+ )}
154
+ </KContainer.View>
155
+ )}
130
156
  <MessageStatusIndicator
131
157
  message={message}
132
158
  isOutgoing={isOutgoing}
133
159
  onRetry={onRetry}
134
160
  />
135
161
  <KContainer.View style={styles.content}>
162
+ {showName && !!message.senderNickname && (
163
+ <KLabel.Text
164
+ typo="TextXsNormal"
165
+ color={KColors.gray.normal}
166
+ numberOfLines={1}
167
+ marginB={2}
168
+ style={styles.senderName}
169
+ >
170
+ {message.senderNickname}
171
+ </KLabel.Text>
172
+ )}
136
173
  <LongPressWrapper onPress={handlePress} onLongPress={handleLongPress}>
137
174
  <MessageComponent
138
175
  message={message}
@@ -141,6 +178,7 @@ export const LegendChatMessage = memo(
141
178
  onMediaPress={hasMediaPress ? onMediaPress : undefined}
142
179
  onPressUrl={hasUrlPress ? onPressUrl : undefined}
143
180
  isHighlighted={isHighlighted}
181
+ groupPosition={groupPosition}
144
182
  />
145
183
  </LongPressWrapper>
146
184
 
@@ -161,10 +199,22 @@ export const LegendChatMessage = memo(
161
199
 
162
200
  LegendChatMessage.displayName = 'LegendChatMessage';
163
201
 
202
+ const AVATAR_SIZE = 24;
203
+
164
204
  const styles = StyleSheet.create({
165
205
  content: {
166
206
  position: 'relative',
167
207
  },
208
+ // Bubbles sit behind a TAIL_WIDTH gutter (see BubbleTailRow); the name has to
209
+ // clear the same gutter or it hangs to the left of the bubble it labels.
210
+ senderName: {
211
+ marginLeft: TAIL_WIDTH,
212
+ },
213
+ avatarGutter: {
214
+ width: AVATAR_SIZE,
215
+ marginRight: KSpacingValue['0.25rem'],
216
+ alignSelf: 'flex-end',
217
+ },
168
218
  systemMessageWrapper: {
169
219
  width: '100%',
170
220
  marginVertical: KSpacingValue['0.25rem'],
@@ -35,11 +35,19 @@ export const MessageToolboxWrapper = memo(
35
35
  toolbox,
36
36
  }: MessageToolboxWrapperProps) => {
37
37
  const insets = useSafeAreaInsets();
38
+ const messageHeight = useSharedValue(0);
38
39
  const reactionHeight = useSharedValue(0);
39
40
  const toolboxHeight = useSharedValue(0);
40
41
  const topOffset = Platform.OS === 'android' ? insets.top : 0;
41
42
  const { height: windowHeight } = useWindowDimensions();
42
43
 
44
+ const onMessageCloneLayout = useCallback(
45
+ ({ nativeEvent }: LayoutChangeEvent) => {
46
+ messageHeight.value = nativeEvent.layout.height;
47
+ },
48
+ [messageHeight]
49
+ );
50
+
43
51
  const onToolboxLayout = useCallback(
44
52
  ({ nativeEvent }: LayoutChangeEvent) => {
45
53
  toolboxHeight.value = nativeEvent.layout.height;
@@ -62,7 +70,7 @@ export const MessageToolboxWrapper = memo(
62
70
  const overflowBottom =
63
71
  Math.max(rect.y, insets.top) +
64
72
  topOffset +
65
- rect.height +
73
+ messageHeight.value +
66
74
  toolboxHeight.value +
67
75
  (insets.bottom || 32) +
68
76
  menuGap -
@@ -89,9 +97,19 @@ export const MessageToolboxWrapper = memo(
89
97
  containerStyles,
90
98
  ]}
91
99
  >
92
- <View pointerEvents="none">{message}</View>
93
- <View onLayout={onReactionLayout}>{reaction}</View>
94
- <View onLayout={onToolboxLayout}>{toolbox}</View>
100
+ <View
101
+ pointerEvents="none"
102
+ collapsable={false}
103
+ onLayout={onMessageCloneLayout}
104
+ >
105
+ {message}
106
+ </View>
107
+ <View collapsable={false} onLayout={onReactionLayout}>
108
+ {reaction}
109
+ </View>
110
+ <View collapsable={false} onLayout={onToolboxLayout}>
111
+ {toolbox}
112
+ </View>
95
113
  </Animated.View>
96
114
  );
97
115
  }
@@ -39,6 +39,7 @@ export const ENDPOINTS = {
39
39
  `${getApiEndpoint('stickersByCategory')}/${id}/stickers`,
40
40
  stickerHistory: () => getApiEndpoint('stickerHistory'),
41
41
  searchMessages: () => `${getApiEndpoint('messages')}/search`,
42
+ searchContacts: () => `${getApiEndpoint('contacts')}/search`,
42
43
  },
43
44
  uploaderService: {
44
45
  get uploadImage() {
@@ -188,7 +188,13 @@ const i18nKey: Record<string, string> = {
188
188
  'Bạn có thể tìm kiếm theo liên hệ hoặc nội dung tin nhắn',
189
189
  'search_message:empty_hint': 'Bạn có thể tìm kiếm theo nội dung tin nhắn',
190
190
  'search_conversation:empty_hint': 'Bạn có thể tìm kiếm theo liên hệ',
191
+ 'search_community:empty_hint': 'Bạn có thể tìm kiếm theo tên nhóm cộng đồng',
191
192
  'view_all': 'Xem tất cả',
193
+ 'contact_type:customer': 'Khách hàng',
194
+ 'contact_type:pdp': 'PDP',
195
+ 'contact_type:group': 'Nhóm',
196
+ 'contact_type:seller': 'Người bán',
197
+ 'group:no_description': 'Chưa có mô tả nhóm',
192
198
  'search_message:revoked_message': 'Tin nhắn đã được thu hồi',
193
199
  'chat:no_permission': 'Bạn không có quyền này',
194
200
  'chat:no_send_permission': 'Bạn không có quyền gửi tin nhắn',