@droppii-org/chat-mobile 0.2.3 → 0.2.6

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 (167) hide show
  1. package/lib/module/components/ThreadCard/NamePrefixIcon.js +2 -3
  2. package/lib/module/components/ThreadCard/NamePrefixIcon.js.map +1 -1
  3. package/lib/module/config/feature-flags.js +38 -0
  4. package/lib/module/config/feature-flags.js.map +1 -0
  5. package/lib/module/hooks/query-keys.js +4 -0
  6. package/lib/module/hooks/query-keys.js.map +1 -1
  7. package/lib/module/hooks/useChatMessages.js +45 -0
  8. package/lib/module/hooks/useChatMessages.js.map +1 -1
  9. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +17 -0
  10. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -0
  11. package/lib/module/hooks/useLinkPreview/useLinkPreview.js +34 -0
  12. package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -0
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/screens/chat-detail/ChatComposer.js +24 -7
  15. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  16. package/lib/module/screens/chat-detail/ChatDetail.js +119 -19
  17. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  18. package/lib/module/screens/chat-detail/ChatDetailHeader.js +43 -20
  19. package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
  20. package/lib/module/screens/chat-detail/ChatLinkPreview.js +79 -0
  21. package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -0
  22. package/lib/module/screens/chat-detail/ChatList.js +2 -0
  23. package/lib/module/screens/chat-detail/ChatList.js.map +1 -1
  24. package/lib/module/screens/chat-detail/ChatListLegend.js +352 -0
  25. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -0
  26. package/lib/module/screens/chat-detail/ChatQuickActions.js +12 -2
  27. package/lib/module/screens/chat-detail/ChatQuickActions.js.map +1 -1
  28. package/lib/module/screens/chat-detail/conversationHeader.utils.js +31 -0
  29. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -0
  30. package/lib/module/screens/chat-detail/index.js +1 -0
  31. package/lib/module/screens/chat-detail/index.js.map +1 -1
  32. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +57 -0
  33. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -0
  34. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js +21 -0
  35. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js.map +1 -0
  36. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -0
  37. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -0
  38. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js +58 -0
  39. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js.map +1 -0
  40. package/lib/module/screens/chat-detail/legend/message-types.js +122 -0
  41. package/lib/module/screens/chat-detail/legend/message-types.js.map +1 -0
  42. package/lib/module/screens/chat-detail/messages/ChatMessageBubble.js.map +1 -1
  43. package/lib/module/services/apis.js +1 -1
  44. package/lib/module/services/apis.js.map +1 -1
  45. package/lib/module/services/endpoints.js +8 -0
  46. package/lib/module/services/endpoints.js.map +1 -0
  47. package/lib/module/types/common.js +2 -0
  48. package/lib/module/types/common.js.map +1 -0
  49. package/lib/module/utils/legendListMessage.js +80 -0
  50. package/lib/module/utils/legendListMessage.js.map +1 -0
  51. package/lib/module/utils/url.js +7 -0
  52. package/lib/module/utils/url.js.map +1 -0
  53. package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
  54. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
  55. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts +1 -1
  56. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts.map +1 -1
  57. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts +1 -1
  58. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
  59. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts +1 -1
  60. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts.map +1 -1
  61. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +1 -1
  62. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  63. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +1 -1
  64. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  65. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts +1 -1
  66. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  67. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts +1 -1
  68. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts.map +1 -1
  69. package/lib/typescript/src/config/feature-flags.d.ts +12 -0
  70. package/lib/typescript/src/config/feature-flags.d.ts.map +1 -0
  71. package/lib/typescript/src/context/ChatContext.d.ts +1 -1
  72. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  73. package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
  74. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  75. package/lib/typescript/src/hooks/useChatMessages.d.ts +3 -0
  76. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  77. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts +3 -0
  78. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts.map +1 -0
  79. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts +7 -0
  80. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -0
  81. package/lib/typescript/src/index.d.ts +1 -1
  82. package/lib/typescript/src/index.d.ts.map +1 -1
  83. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts +1 -1
  84. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts.map +1 -1
  85. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +1 -1
  86. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  87. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts +1 -1
  88. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts.map +1 -1
  89. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  90. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  91. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  92. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts +9 -0
  94. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts.map +1 -0
  95. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts +1 -1
  96. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts.map +1 -1
  97. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +3 -0
  98. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -0
  99. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts +1 -1
  100. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts.map +1 -1
  101. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts +1 -1
  102. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts +1 -1
  104. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts.map +1 -1
  105. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts +1 -1
  106. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts.map +1 -1
  107. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +6 -0
  108. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -0
  109. package/lib/typescript/src/screens/chat-detail/index.d.ts +2 -1
  110. package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
  111. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -0
  112. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -0
  113. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts +6 -0
  114. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts.map +1 -0
  115. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +15 -0
  116. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -0
  117. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts +6 -0
  118. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts.map +1 -0
  119. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts +12 -0
  120. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts.map +1 -0
  121. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts +1 -1
  122. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts.map +1 -1
  123. package/lib/typescript/src/screens/chat-detail/types.d.ts +34 -5
  124. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  125. package/lib/typescript/src/screens/inbox/Inbox.d.ts +1 -1
  126. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  127. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts +1 -1
  128. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  129. package/lib/typescript/src/services/apis.d.ts +1 -0
  130. package/lib/typescript/src/services/apis.d.ts.map +1 -1
  131. package/lib/typescript/src/services/endpoints.d.ts +6 -0
  132. package/lib/typescript/src/services/endpoints.d.ts.map +1 -0
  133. package/lib/typescript/src/types/common.d.ts +6 -0
  134. package/lib/typescript/src/types/common.d.ts.map +1 -0
  135. package/lib/typescript/src/utils/legendListMessage.d.ts +25 -0
  136. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -0
  137. package/lib/typescript/src/utils/url.d.ts +2 -0
  138. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  139. package/package.json +4 -2
  140. package/src/components/ThreadCard/NamePrefixIcon.tsx +2 -3
  141. package/src/config/feature-flags.ts +49 -0
  142. package/src/hooks/query-keys.ts +5 -0
  143. package/src/hooks/useChatMessages.ts +60 -0
  144. package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +18 -0
  145. package/src/hooks/useLinkPreview/useLinkPreview.ts +30 -0
  146. package/src/index.tsx +1 -0
  147. package/src/screens/chat-detail/ChatComposer.tsx +30 -9
  148. package/src/screens/chat-detail/ChatDetail.tsx +163 -27
  149. package/src/screens/chat-detail/ChatDetailHeader.tsx +58 -26
  150. package/src/screens/chat-detail/ChatLinkPreview.tsx +86 -0
  151. package/src/screens/chat-detail/ChatList.tsx +3 -0
  152. package/src/screens/chat-detail/ChatListLegend.tsx +404 -0
  153. package/src/screens/chat-detail/ChatQuickActions.tsx +19 -2
  154. package/src/screens/chat-detail/conversationHeader.utils.ts +52 -0
  155. package/src/screens/chat-detail/index.ts +7 -0
  156. package/src/screens/chat-detail/legend/LegendChatDay.tsx +70 -0
  157. package/src/screens/chat-detail/legend/LegendChatLoadEarlier.tsx +21 -0
  158. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +66 -0
  159. package/src/screens/chat-detail/legend/LegendChatScrollToBottom.tsx +56 -0
  160. package/src/screens/chat-detail/legend/message-types.tsx +149 -0
  161. package/src/screens/chat-detail/messages/ChatMessageBubble.tsx +0 -1
  162. package/src/screens/chat-detail/types.ts +47 -5
  163. package/src/services/apis.ts +1 -1
  164. package/src/services/endpoints.ts +5 -0
  165. package/src/types/common.ts +5 -0
  166. package/src/utils/legendListMessage.ts +102 -0
  167. package/src/utils/url.ts +5 -0
@@ -29,24 +29,33 @@ export function useChatMessages({
29
29
  const [isLoading, setIsLoading] = useState(false);
30
30
  const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
31
31
  const [hasMoreEarlier, setHasMoreEarlier] = useState(false);
32
+ const [isLoadingNewer, setIsLoadingNewer] = useState(false);
33
+ const [hasMoreNewer, setHasMoreNewer] = useState(false);
32
34
  const [error, setError] = useState<Error | null>(null);
33
35
 
34
36
  const conversationIdRef = useRef(conversationId);
35
37
  conversationIdRef.current = conversationId;
36
38
 
37
39
  const isLoadingEarlierRef = useRef(false);
40
+ const isLoadingNewerRef = useRef(false);
38
41
  const messagesRef = useRef(messages);
39
42
  const hasMoreEarlierRef = useRef(hasMoreEarlier);
43
+ const hasMoreNewerRef = useRef(hasMoreNewer);
40
44
  const historyAnchorRef = useRef<HistoryPaginationAnchor | null>(null);
45
+ const newestAnchorRef = useRef<HistoryPaginationAnchor | null>(null);
41
46
  messagesRef.current = messages;
42
47
  hasMoreEarlierRef.current = hasMoreEarlier;
48
+ hasMoreNewerRef.current = hasMoreNewer;
43
49
 
44
50
  const resetState = useCallback(() => {
45
51
  setMessages([]);
46
52
  setHasMoreEarlier(false);
53
+ setHasMoreNewer(false);
47
54
  setError(null);
48
55
  isLoadingEarlierRef.current = false;
56
+ isLoadingNewerRef.current = false;
49
57
  historyAnchorRef.current = null;
58
+ newestAnchorRef.current = null;
50
59
  }, []);
51
60
 
52
61
  const loadInitialMessages = useCallback(async () => {
@@ -154,6 +163,54 @@ export function useChatMessages({
154
163
  }
155
164
  }, [conversationId, pageSize]);
156
165
 
166
+ const onLoadNewer = useCallback(async () => {
167
+ if (
168
+ !conversationId ||
169
+ isLoadingNewerRef.current ||
170
+ !hasMoreNewerRef.current ||
171
+ !messagesRef.current.length
172
+ ) {
173
+ return;
174
+ }
175
+
176
+ isLoadingNewerRef.current = true;
177
+ setIsLoadingNewer(true);
178
+ setError(null);
179
+
180
+ const anchor = newestAnchorRef.current;
181
+ if (!anchor?.clientMsgID && messagesRef.current.length === 0) {
182
+ isLoadingNewerRef.current = false;
183
+ setIsLoadingNewer(false);
184
+ return;
185
+ }
186
+
187
+ try {
188
+ // Get the newest message ID to load from
189
+ const newestMessage = messagesRef.current[messagesRef.current.length - 1];
190
+ const history = await ChatMessageAPI.fetchHistoryMessages(
191
+ conversationId,
192
+ {
193
+ count: pageSize,
194
+ startClientMsgID: newestMessage?.clientMsgID,
195
+ }
196
+ );
197
+
198
+ const incoming = history.messageList as DMessageItem[];
199
+ const hasNewMessages = incoming.length > 0;
200
+
201
+ if (hasNewMessages) {
202
+ setMessages((current) => mergeMessages(current, incoming));
203
+ }
204
+
205
+ setHasMoreNewer(!history.isEnd && hasNewMessages);
206
+ } catch (err) {
207
+ setError(err instanceof Error ? err : new Error(String(err)));
208
+ } finally {
209
+ isLoadingNewerRef.current = false;
210
+ setIsLoadingNewer(false);
211
+ }
212
+ }, [conversationId, pageSize]);
213
+
157
214
  const sendTextMessage = useCallback(async (text: string) => {
158
215
  const trimmed = text.trim();
159
216
  if (!trimmed) {
@@ -223,8 +280,11 @@ export function useChatMessages({
223
280
  isLoading,
224
281
  isLoadingEarlier,
225
282
  hasMoreEarlier,
283
+ isLoadingNewer,
284
+ hasMoreNewer,
226
285
  error,
227
286
  onLoadEarlier,
287
+ onLoadNewer,
228
288
  sendTextMessage,
229
289
  refresh: loadInitialMessages,
230
290
  };
@@ -0,0 +1,18 @@
1
+ import { keepPreviousData, useQuery } from '@tanstack/react-query';
2
+ import { commonQueryKeys } from '../query-keys';
3
+ import { apiInstance } from '../../services/apis';
4
+ import type { BaseResponse } from '../../types/auth';
5
+ import type { IUrlMetadata } from '../../types/common';
6
+ import { ENDPOINTS } from '../../services/endpoints';
7
+
8
+ export const useFetchUrlMetadata = (url?: string) =>
9
+ useQuery({
10
+ queryKey: commonQueryKeys.urlMetadata(url || ''),
11
+ queryFn: async () => {
12
+ const path = `${ENDPOINTS.chatService.urlMetadata}?url=${url}`;
13
+ const res = await apiInstance?.get<BaseResponse<IUrlMetadata>>(path);
14
+ return res?.data?.data;
15
+ },
16
+ enabled: !!url,
17
+ placeholderData: keepPreviousData,
18
+ });
@@ -0,0 +1,30 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useFetchUrlMetadata } from './useFetchUrlMetadata';
3
+ import { extractFirstUrl } from '../../utils/url';
4
+
5
+ export function useLinkPreview(value: string | undefined) {
6
+ const [detectedUrl, setDetectedUrl] = useState<string | undefined>(undefined);
7
+ const [isDismissed, setIsDismissed] = useState(false);
8
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
9
+
10
+ useEffect(() => {
11
+ if (timerRef.current) clearTimeout(timerRef.current);
12
+ timerRef.current = setTimeout(() => {
13
+ const url = value ? extractFirstUrl(value) : undefined;
14
+ setDetectedUrl(url);
15
+ if (url) setIsDismissed(false);
16
+ }, 500);
17
+ return () => {
18
+ if (timerRef.current) clearTimeout(timerRef.current);
19
+ };
20
+ }, [value]);
21
+
22
+ const { data: metadata, isFetching: isLoading } =
23
+ useFetchUrlMetadata(detectedUrl);
24
+
25
+ const dismiss = useCallback(() => setIsDismissed(true), []);
26
+
27
+ const isVisible = !!detectedUrl && !isDismissed;
28
+
29
+ return { isVisible, metadata, isLoading, dismiss };
30
+ }
package/src/index.tsx CHANGED
@@ -28,6 +28,7 @@ export type {
28
28
  ChatListProps,
29
29
  ChatComposerProps,
30
30
  ChatQuickActionsProps,
31
+ ChatQuickActionsRenderParams,
31
32
  ChatAttachmentPanelProps,
32
33
  } from './screens/chat-detail';
33
34
  export type { DGiftedChatMessage } from './utils/giftedChatMessage';
@@ -22,8 +22,10 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
22
22
  import { KContainer, KImage, KColors, KSpacingValue } from '@droppii/libs';
23
23
  import { ChatQuickActions } from './ChatQuickActions';
24
24
  import { ChatAttachmentPanel } from './ChatAttachmentPanel';
25
+ import { ChatLinkPreview } from './ChatLinkPreview';
25
26
  import { getAttachmentPanelHeight } from './constants';
26
27
  import type { ChatComposerProps } from './types';
28
+ import { useLinkPreview } from '../../hooks/useLinkPreview/useLinkPreview';
27
29
 
28
30
  const ACCESSORY_ANIMATION_MS = 250;
29
31
  const ACCESSORY_SETTLE_MS = 150;
@@ -31,6 +33,10 @@ const IS_IOS = Platform.OS === 'ios';
31
33
  const INPUT_FONT_SIZE = 16;
32
34
  const INPUT_LINE_HEIGHT = 22.4;
33
35
  const INPUT_MAX_HEIGHT = INPUT_LINE_HEIGHT * 5;
36
+ // Android's onContentSizeChange includes paddingVertical (4*2=8) in reported height
37
+ const WRAPPED_HEIGHT_THRESHOLD = IS_IOS
38
+ ? INPUT_LINE_HEIGHT + 4
39
+ : INPUT_LINE_HEIGHT + 12;
34
40
 
35
41
  export const ChatComposer = memo(
36
42
  ({
@@ -40,11 +46,13 @@ export const ChatComposer = memo(
40
46
  onSend,
41
47
  onPressAttach,
42
48
  onPressEmoji,
49
+ showQuickActions = true,
43
50
  quickActions,
44
51
  attachmentActions,
45
52
  onQuickActionPress,
46
53
  onAttachmentAction,
47
54
  renderQuickAction,
55
+ renderQuickActions,
48
56
  renderAttachmentAction,
49
57
  attachmentColumns,
50
58
  }: ChatComposerProps) => {
@@ -56,6 +64,13 @@ export const ChatComposer = memo(
56
64
  const [isInputWrapped, setIsInputWrapped] = useState(false);
57
65
  const insets = useSafeAreaInsets();
58
66
 
67
+ const {
68
+ isVisible: showLinkPreview,
69
+ metadata: urlMetadata,
70
+ isLoading: isMetadataLoading,
71
+ dismiss: handleDismissPreview,
72
+ } = useLinkPreview(value);
73
+
59
74
  const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
60
75
  const keyboardHeight = useKeyboardState((state) => state.height);
61
76
  const { progress: keyboardProgress } = useReanimatedKeyboardAnimation();
@@ -221,7 +236,7 @@ export const ChatComposer = memo(
221
236
  const handleInputContentSizeChange = useCallback(
222
237
  (event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
223
238
  const { height } = event.nativeEvent.contentSize;
224
- setIsInputWrapped(height > INPUT_LINE_HEIGHT + 4);
239
+ setIsInputWrapped(height > WRAPPED_HEIGHT_THRESHOLD);
225
240
  },
226
241
  []
227
242
  );
@@ -242,14 +257,10 @@ export const ChatComposer = memo(
242
257
 
243
258
  const renderEmojiNode = useCallback(
244
259
  () => (
245
- <KContainer.Touchable
246
- marginR="0.5rem"
247
- onPress={onPressEmoji}
248
- activeOpacity={0.7}
249
- >
260
+ <KContainer.Touchable marginR="0.5rem" onPress={onPressEmoji}>
250
261
  <KImage.VectorIcons
251
262
  name="sticker-o"
252
- size={22}
263
+ size={24}
253
264
  color={KColors.gray.normal}
254
265
  />
255
266
  </KContainer.Touchable>
@@ -268,11 +279,21 @@ export const ChatComposer = memo(
268
279
  return (
269
280
  <KContainer.View background={'rgba(131, 137, 157, 0.05)'}>
270
281
  <ChatQuickActions
282
+ visible={showQuickActions}
271
283
  actions={quickActions}
272
284
  onActionPress={onQuickActionPress}
273
285
  renderAction={renderQuickAction}
286
+ renderQuickActions={renderQuickActions}
274
287
  />
275
288
 
289
+ {showLinkPreview ? (
290
+ <ChatLinkPreview
291
+ metadata={urlMetadata}
292
+ isLoading={isMetadataLoading}
293
+ onDismiss={handleDismissPreview}
294
+ />
295
+ ) : null}
296
+
276
297
  <KContainer.View
277
298
  row
278
299
  alignItems
@@ -397,7 +418,7 @@ const styles = StyleSheet.create({
397
418
  inputContainer: {
398
419
  borderRadius: KSpacingValue['1.25rem'],
399
420
  minHeight: 48,
400
- paddingVertical: IS_IOS ? 4 : 0,
421
+ paddingVertical: KSpacingValue['0.25rem'],
401
422
  },
402
423
  textInput: {
403
424
  flex: 1,
@@ -407,6 +428,6 @@ const styles = StyleSheet.create({
407
428
  color: KColors.palette.gray.w900,
408
429
  paddingVertical: IS_IOS ? 0 : 4,
409
430
  paddingHorizontal: IS_IOS ? 2 : 0,
410
- marginVertical: IS_IOS ? 0 : -8,
431
+ includeFontPadding: false,
411
432
  },
412
433
  });
@@ -1,47 +1,142 @@
1
- import { memo, useState, useCallback } from 'react';
2
- import { StyleSheet } from 'react-native';
1
+ import { memo, useState, useCallback, useEffect, useMemo } from 'react';
2
+ import { Platform, StyleSheet } from 'react-native';
3
+
3
4
  import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
6
  import { KContainer } from '@droppii/libs';
7
+ import { useChatMessages } from '../../hooks/useChatMessages';
8
+ import { useConversation, useConversationStore } from '../../store';
5
9
  import ChatDetailHeader from './ChatDetailHeader';
6
10
  import { ChatList } from './ChatList';
11
+ import { ChatListLegend } from './ChatListLegend';
7
12
  import { ChatComposer } from './ChatComposer';
13
+ import {
14
+ DEFAULT_ATTACHMENT_ACTIONS,
15
+ DEFAULT_Chat_QUICK_ACTIONS,
16
+ } from './constants';
17
+ import {
18
+ getConversationAvatarUri,
19
+ getConversationSubtitle,
20
+ getConversationTitle,
21
+ shouldShowAddMember,
22
+ } from './conversationHeader.utils';
23
+ import { getFeatureFlag, onFlagsChange } from '../../config/feature-flags';
8
24
  import type { ChatDetailProps } from './types';
9
25
 
10
26
  const ChatDetail = memo(
11
27
  ({
28
+ conversationId,
29
+ enabled = true,
12
30
  title,
13
31
  subtitle,
14
32
  avatarUri,
15
33
  avatarFullName,
16
34
  chatType,
35
+ chatCategory,
36
+ applicationType,
17
37
  showAddMember,
38
+ getSubtitle,
18
39
  onBack,
19
40
  onPressSearch,
20
41
  onPressAddMember,
21
42
  onPressMenu,
22
43
  onPressAvatar,
23
- messages = [],
24
- currentUserId,
25
44
  renderChat,
26
- onLoadEarlier,
27
- isLoading,
28
- isLoadingEarlier,
29
- hasMoreEarlier,
45
+ showQuickActions = true,
46
+ showAttachmentActions = true,
30
47
  quickActions,
48
+ getQuickActions,
31
49
  inputValue,
32
50
  inputPlaceholder,
33
51
  onChangeInput,
34
- onSend,
35
52
  onPressAttach,
36
53
  onPressEmoji,
37
54
  attachmentActions,
38
55
  onQuickActionPress,
39
56
  onAttachmentAction,
40
57
  renderQuickAction,
58
+ renderQuickActions,
41
59
  renderAttachmentAction,
42
60
  attachmentColumns,
43
61
  }: ChatDetailProps) => {
44
62
  const [internalInput, setInternalInput] = useState('');
63
+ const conversation = useConversation(conversationId);
64
+
65
+ const {
66
+ messages,
67
+ currentUserId,
68
+ onLoadEarlier,
69
+ sendTextMessage,
70
+ isLoading,
71
+ isLoadingEarlier,
72
+ hasMoreEarlier,
73
+ } = useChatMessages({
74
+ conversationId,
75
+ enabled,
76
+ });
77
+
78
+ useEffect(() => {
79
+ useConversationStore.getState().setCurrentConversation(conversationId);
80
+ }, [conversationId]);
81
+
82
+ const resolvedTitle = title ?? getConversationTitle(conversation);
83
+ const resolvedAvatarUri =
84
+ avatarUri ?? getConversationAvatarUri(conversation);
85
+ const resolvedAvatarFullName = avatarFullName ?? resolvedTitle;
86
+ const resolvedChatType = chatType ?? conversation?.chatType;
87
+ const resolvedChatCategory = chatCategory ?? conversation?.chatCategory;
88
+ const resolvedApplicationType =
89
+ applicationType ?? conversation?.applicationType;
90
+ const resolvedShowAddMember =
91
+ showAddMember ?? shouldShowAddMember(conversation);
92
+ const resolvedSubtitle = useMemo(() => {
93
+ if (subtitle !== undefined) {
94
+ return subtitle;
95
+ }
96
+
97
+ if (getSubtitle && conversation) {
98
+ return getSubtitle(conversation);
99
+ }
100
+
101
+ return getConversationSubtitle(conversation);
102
+ }, [conversation, getSubtitle, subtitle]);
103
+
104
+ const resolvedQuickActions = useMemo(() => {
105
+ if (!showQuickActions) {
106
+ return undefined;
107
+ }
108
+
109
+ if (quickActions) {
110
+ return quickActions;
111
+ }
112
+
113
+ if (getQuickActions && conversation) {
114
+ return getQuickActions(conversation);
115
+ }
116
+
117
+ return DEFAULT_Chat_QUICK_ACTIONS;
118
+ }, [conversation, getQuickActions, quickActions, showQuickActions]);
119
+
120
+ const resolvedAttachmentActions = useMemo(() => {
121
+ if (!showAttachmentActions) {
122
+ return undefined;
123
+ }
124
+
125
+ return attachmentActions ?? DEFAULT_ATTACHMENT_ACTIONS;
126
+ }, [attachmentActions, showAttachmentActions]);
127
+ const [variant, setVariant] = useState<'gifted' | 'legend'>(
128
+ () => getFeatureFlag('CHAT_LIST_VARIANT') as 'gifted' | 'legend'
129
+ );
130
+
131
+ useEffect(() => {
132
+ const unsubscribe = onFlagsChange(() => {
133
+ const newVariant = getFeatureFlag('CHAT_LIST_VARIANT') as
134
+ | 'gifted'
135
+ | 'legend';
136
+ setVariant(newVariant);
137
+ });
138
+ return unsubscribe;
139
+ }, []);
45
140
 
46
141
  const handleChangeInput = useCallback(
47
142
  (text: string) => {
@@ -56,15 +151,38 @@ const ChatDetail = memo(
56
151
 
57
152
  const currentInput = inputValue ?? internalInput;
58
153
 
154
+ const handleSend = useCallback(async () => {
155
+ const text = currentInput.trim();
156
+ if (!text) {
157
+ return;
158
+ }
159
+
160
+ try {
161
+ await sendTextMessage(text);
162
+
163
+ if (onChangeInput) {
164
+ onChangeInput('');
165
+ } else {
166
+ setInternalInput('');
167
+ }
168
+ } catch (error) {
169
+ console.error('[send-message]', error);
170
+ }
171
+ }, [currentInput, onChangeInput, sendTextMessage]);
172
+
173
+ const insets = useSafeAreaInsets();
174
+
59
175
  return (
60
176
  <KContainer.Page flex edges={['bottom']}>
61
177
  <ChatDetailHeader
62
- title={title}
63
- subtitle={subtitle}
64
- avatarUri={avatarUri}
65
- avatarFullName={avatarFullName}
66
- chatType={chatType}
67
- showAddMember={showAddMember}
178
+ title={resolvedTitle}
179
+ subtitle={resolvedSubtitle}
180
+ avatarUri={resolvedAvatarUri}
181
+ avatarFullName={resolvedAvatarFullName}
182
+ chatType={resolvedChatType}
183
+ chatCategory={resolvedChatCategory}
184
+ applicationType={resolvedApplicationType}
185
+ showAddMember={resolvedShowAddMember}
68
186
  onBack={onBack}
69
187
  onPressSearch={onPressSearch}
70
188
  onPressAddMember={onPressAddMember}
@@ -75,29 +193,47 @@ const ChatDetail = memo(
75
193
  <KeyboardAvoidingView
76
194
  style={styles.keyboardAvoiding}
77
195
  behavior="padding"
196
+ keyboardVerticalOffset={Platform.select({
197
+ android: insets.bottom,
198
+ default: 0,
199
+ })}
78
200
  >
79
- <ChatList
80
- messages={messages}
81
- currentUserId={currentUserId}
82
- renderChat={renderChat}
83
- onLoadEarlier={onLoadEarlier}
84
- isLoading={isLoading}
85
- isLoadingEarlier={isLoadingEarlier}
86
- hasMoreEarlier={hasMoreEarlier}
87
- />
201
+ {variant === 'legend' ? (
202
+ <ChatListLegend
203
+ messages={messages}
204
+ currentUserId={currentUserId}
205
+ renderChat={renderChat}
206
+ onLoadEarlier={onLoadEarlier}
207
+ isLoading={isLoading}
208
+ isLoadingEarlier={isLoadingEarlier}
209
+ hasMoreEarlier={hasMoreEarlier}
210
+ />
211
+ ) : (
212
+ <ChatList
213
+ messages={messages}
214
+ currentUserId={currentUserId}
215
+ renderChat={renderChat}
216
+ onLoadEarlier={onLoadEarlier}
217
+ isLoading={isLoading}
218
+ isLoadingEarlier={isLoadingEarlier}
219
+ hasMoreEarlier={hasMoreEarlier}
220
+ />
221
+ )}
88
222
 
89
223
  <ChatComposer
90
224
  value={currentInput}
91
225
  placeholder={inputPlaceholder}
92
226
  onChangeText={handleChangeInput}
93
- onSend={onSend}
227
+ onSend={handleSend}
94
228
  onPressAttach={onPressAttach}
95
229
  onPressEmoji={onPressEmoji}
96
- quickActions={quickActions}
97
- attachmentActions={attachmentActions}
230
+ showQuickActions={showQuickActions}
231
+ quickActions={resolvedQuickActions}
232
+ attachmentActions={resolvedAttachmentActions}
98
233
  onQuickActionPress={onQuickActionPress}
99
234
  onAttachmentAction={onAttachmentAction}
100
235
  renderQuickAction={renderQuickAction}
236
+ renderQuickActions={renderQuickActions}
101
237
  renderAttachmentAction={renderAttachmentAction}
102
238
  attachmentColumns={attachmentColumns}
103
239
  />
@@ -6,7 +6,8 @@ import {
6
6
  KLabel,
7
7
  KColors,
8
8
  } from '@droppii/libs';
9
- import { Avatar } from '../../components/Avatar';
9
+ import { AvatarSection } from '../../components/ThreadCard/AvatarSection';
10
+ import { NamePrefixIcon } from '../../components/ThreadCard/NamePrefixIcon';
10
11
  import type { ChatDetailHeaderProps } from './types';
11
12
 
12
13
  const ChatDetailHeader = memo(
@@ -15,6 +16,9 @@ const ChatDetailHeader = memo(
15
16
  subtitle,
16
17
  avatarUri,
17
18
  avatarFullName,
19
+ chatType,
20
+ chatCategory,
21
+ applicationType,
18
22
  showAddMember = false,
19
23
  onBack,
20
24
  onPressSearch,
@@ -58,7 +62,7 @@ const ChatDetailHeader = memo(
58
62
 
59
63
  const leftNode = useMemo(
60
64
  () => (
61
- <KContainer.View row alignItems marginR="0.25rem">
65
+ <KContainer.View row alignItems flex marginR="0.25rem">
62
66
  <KContainer.Touchable onPress={onBack} height={40} width={40} center>
63
67
  <KImage.VectorIcons
64
68
  name="arrow-left-o"
@@ -67,41 +71,69 @@ const ChatDetailHeader = memo(
67
71
  />
68
72
  </KContainer.Touchable>
69
73
 
70
- <Avatar
71
- source={avatarUri}
72
- fullName={avatarFullName ?? title}
73
- size="md"
74
+ <KContainer.Touchable
75
+ row
76
+ alignItems
77
+ flex
74
78
  onPress={onPressAvatar}
75
- />
79
+ activeOpacity={0.7}
80
+ >
81
+ <AvatarSection
82
+ avatar={avatarUri ?? null}
83
+ fullName={avatarFullName ?? title}
84
+ chatCategory={chatCategory}
85
+ applicationType={applicationType}
86
+ chatType={chatType}
87
+ />
88
+
89
+ <KContainer.View flex marginL="0.5rem">
90
+ <KContainer.View row alignItems>
91
+ <NamePrefixIcon
92
+ chatCategory={chatCategory}
93
+ chatType={chatType}
94
+ />
95
+ <KLabel.Text
96
+ typo="TextLgBold"
97
+ color={KColors.gray.dark}
98
+ numberOfLines={1}
99
+ flex
100
+ >
101
+ {title}
102
+ </KLabel.Text>
103
+ </KContainer.View>
104
+ {!!subtitle && (
105
+ <KLabel.Text
106
+ typo="TextSmNormal"
107
+ color={KColors.palette.gray.w500}
108
+ numberOfLines={1}
109
+ >
110
+ {subtitle}
111
+ </KLabel.Text>
112
+ )}
113
+ </KContainer.View>
114
+ </KContainer.Touchable>
76
115
  </KContainer.View>
77
116
  ),
78
- [avatarFullName, avatarUri, onBack, onPressAvatar, title]
117
+ [
118
+ avatarFullName,
119
+ avatarUri,
120
+ chatCategory,
121
+ applicationType,
122
+ chatType,
123
+ onBack,
124
+ onPressAvatar,
125
+ subtitle,
126
+ title,
127
+ ]
79
128
  );
80
129
 
81
- const subTitleNode = useMemo(() => {
82
- if (!subtitle) {
83
- return null;
84
- }
85
-
86
- return (
87
- <KLabel.Text
88
- typo="TextSmNormal"
89
- color={KColors.palette.gray.w500}
90
- numberOfLines={1}
91
- >
92
- {subtitle}
93
- </KLabel.Text>
94
- );
95
- }, [subtitle]);
96
-
97
130
  return (
98
131
  <KNavigation.Header
99
132
  theme="light"
100
133
  alignment="left"
101
134
  size="small"
102
135
  leftNode={leftNode}
103
- title={title}
104
- subTitle={subTitleNode}
136
+ disableCenterSpace
105
137
  initRightButtons={rightButtons}
106
138
  shadow
107
139
  />