@droppii-org/chat-mobile 0.2.4 → 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 (161) hide show
  1. package/lib/module/config/feature-flags.js +38 -0
  2. package/lib/module/config/feature-flags.js.map +1 -0
  3. package/lib/module/hooks/query-keys.js +4 -0
  4. package/lib/module/hooks/query-keys.js.map +1 -1
  5. package/lib/module/hooks/useChatMessages.js +45 -0
  6. package/lib/module/hooks/useChatMessages.js.map +1 -1
  7. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js +17 -0
  8. package/lib/module/hooks/useLinkPreview/useFetchUrlMetadata.js.map +1 -0
  9. package/lib/module/hooks/useLinkPreview/useLinkPreview.js +34 -0
  10. package/lib/module/hooks/useLinkPreview/useLinkPreview.js.map +1 -0
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/screens/chat-detail/ChatComposer.js +18 -2
  13. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  14. package/lib/module/screens/chat-detail/ChatDetail.js +110 -20
  15. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  16. package/lib/module/screens/chat-detail/ChatLinkPreview.js +79 -0
  17. package/lib/module/screens/chat-detail/ChatLinkPreview.js.map +1 -0
  18. package/lib/module/screens/chat-detail/ChatList.js +2 -0
  19. package/lib/module/screens/chat-detail/ChatList.js.map +1 -1
  20. package/lib/module/screens/chat-detail/ChatListLegend.js +352 -0
  21. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -0
  22. package/lib/module/screens/chat-detail/ChatQuickActions.js +12 -2
  23. package/lib/module/screens/chat-detail/ChatQuickActions.js.map +1 -1
  24. package/lib/module/screens/chat-detail/conversationHeader.utils.js +31 -0
  25. package/lib/module/screens/chat-detail/conversationHeader.utils.js.map +1 -0
  26. package/lib/module/screens/chat-detail/index.js +1 -0
  27. package/lib/module/screens/chat-detail/index.js.map +1 -1
  28. package/lib/module/screens/chat-detail/legend/LegendChatDay.js +57 -0
  29. package/lib/module/screens/chat-detail/legend/LegendChatDay.js.map +1 -0
  30. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js +21 -0
  31. package/lib/module/screens/chat-detail/legend/LegendChatLoadEarlier.js.map +1 -0
  32. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +47 -0
  33. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -0
  34. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js +58 -0
  35. package/lib/module/screens/chat-detail/legend/LegendChatScrollToBottom.js.map +1 -0
  36. package/lib/module/screens/chat-detail/legend/message-types.js +122 -0
  37. package/lib/module/screens/chat-detail/legend/message-types.js.map +1 -0
  38. package/lib/module/screens/chat-detail/messages/ChatMessageBubble.js.map +1 -1
  39. package/lib/module/services/apis.js +1 -1
  40. package/lib/module/services/apis.js.map +1 -1
  41. package/lib/module/services/endpoints.js +8 -0
  42. package/lib/module/services/endpoints.js.map +1 -0
  43. package/lib/module/types/common.js +2 -0
  44. package/lib/module/types/common.js.map +1 -0
  45. package/lib/module/utils/legendListMessage.js +80 -0
  46. package/lib/module/utils/legendListMessage.js.map +1 -0
  47. package/lib/module/utils/url.js +7 -0
  48. package/lib/module/utils/url.js.map +1 -0
  49. package/lib/typescript/src/components/Avatar/Avatar.d.ts +1 -1
  50. package/lib/typescript/src/components/Avatar/Avatar.d.ts.map +1 -1
  51. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts +1 -1
  52. package/lib/typescript/src/components/Avatar/AvatarBadge.d.ts.map +1 -1
  53. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts +1 -1
  54. package/lib/typescript/src/components/Avatar/DoubleAvatar.d.ts.map +1 -1
  55. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts +1 -1
  56. package/lib/typescript/src/components/Avatar/SingleAvatar.d.ts.map +1 -1
  57. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts +1 -1
  58. package/lib/typescript/src/components/ThreadCard/AvatarSection.d.ts.map +1 -1
  59. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts +1 -1
  60. package/lib/typescript/src/components/ThreadCard/NamePrefixIcon.d.ts.map +1 -1
  61. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts +1 -1
  62. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  63. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts +1 -1
  64. package/lib/typescript/src/components/ThreadCard/UnreadBadge.d.ts.map +1 -1
  65. package/lib/typescript/src/config/feature-flags.d.ts +12 -0
  66. package/lib/typescript/src/config/feature-flags.d.ts.map +1 -0
  67. package/lib/typescript/src/context/ChatContext.d.ts +1 -1
  68. package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
  69. package/lib/typescript/src/hooks/query-keys.d.ts +4 -0
  70. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  71. package/lib/typescript/src/hooks/useChatMessages.d.ts +3 -0
  72. package/lib/typescript/src/hooks/useChatMessages.d.ts.map +1 -1
  73. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts +3 -0
  74. package/lib/typescript/src/hooks/useLinkPreview/useFetchUrlMetadata.d.ts.map +1 -0
  75. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts +7 -0
  76. package/lib/typescript/src/hooks/useLinkPreview/useLinkPreview.d.ts.map +1 -0
  77. package/lib/typescript/src/index.d.ts +1 -1
  78. package/lib/typescript/src/index.d.ts.map +1 -1
  79. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts +1 -1
  80. package/lib/typescript/src/screens/chat-detail/ChatAttachmentPanel.d.ts.map +1 -1
  81. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +1 -1
  82. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  83. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts +1 -1
  84. package/lib/typescript/src/screens/chat-detail/ChatDay.d.ts.map +1 -1
  85. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  86. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  87. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
  88. package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
  89. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts +9 -0
  90. package/lib/typescript/src/screens/chat-detail/ChatLinkPreview.d.ts.map +1 -0
  91. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts +1 -1
  92. package/lib/typescript/src/screens/chat-detail/ChatList.d.ts.map +1 -1
  93. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +3 -0
  94. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -0
  95. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts +1 -1
  96. package/lib/typescript/src/screens/chat-detail/ChatLoadEarlier.d.ts.map +1 -1
  97. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts +1 -1
  98. package/lib/typescript/src/screens/chat-detail/ChatQuickActions.d.ts.map +1 -1
  99. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts +1 -1
  100. package/lib/typescript/src/screens/chat-detail/ChatScrollToBottom.d.ts.map +1 -1
  101. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts +1 -1
  102. package/lib/typescript/src/screens/chat-detail/ChatTextBubble.d.ts.map +1 -1
  103. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts +6 -0
  104. package/lib/typescript/src/screens/chat-detail/conversationHeader.utils.d.ts.map +1 -0
  105. package/lib/typescript/src/screens/chat-detail/index.d.ts +2 -1
  106. package/lib/typescript/src/screens/chat-detail/index.d.ts.map +1 -1
  107. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts +6 -0
  108. package/lib/typescript/src/screens/chat-detail/legend/LegendChatDay.d.ts.map +1 -0
  109. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts +6 -0
  110. package/lib/typescript/src/screens/chat-detail/legend/LegendChatLoadEarlier.d.ts.map +1 -0
  111. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +15 -0
  112. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -0
  113. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts +6 -0
  114. package/lib/typescript/src/screens/chat-detail/legend/LegendChatScrollToBottom.d.ts.map +1 -0
  115. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts +12 -0
  116. package/lib/typescript/src/screens/chat-detail/legend/message-types.d.ts.map +1 -0
  117. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts +1 -1
  118. package/lib/typescript/src/screens/chat-detail/messages/ChatMessageBubble.d.ts.map +1 -1
  119. package/lib/typescript/src/screens/chat-detail/types.d.ts +30 -3
  120. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  121. package/lib/typescript/src/screens/inbox/Inbox.d.ts +1 -1
  122. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  123. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts +1 -1
  124. package/lib/typescript/src/screens/inbox/MessagesTab.d.ts.map +1 -1
  125. package/lib/typescript/src/services/apis.d.ts +1 -0
  126. package/lib/typescript/src/services/apis.d.ts.map +1 -1
  127. package/lib/typescript/src/services/endpoints.d.ts +6 -0
  128. package/lib/typescript/src/services/endpoints.d.ts.map +1 -0
  129. package/lib/typescript/src/types/common.d.ts +6 -0
  130. package/lib/typescript/src/types/common.d.ts.map +1 -0
  131. package/lib/typescript/src/utils/legendListMessage.d.ts +25 -0
  132. package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -0
  133. package/lib/typescript/src/utils/url.d.ts +2 -0
  134. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  135. package/package.json +4 -2
  136. package/src/config/feature-flags.ts +49 -0
  137. package/src/hooks/query-keys.ts +5 -0
  138. package/src/hooks/useChatMessages.ts +60 -0
  139. package/src/hooks/useLinkPreview/useFetchUrlMetadata.ts +18 -0
  140. package/src/hooks/useLinkPreview/useLinkPreview.ts +30 -0
  141. package/src/index.tsx +1 -0
  142. package/src/screens/chat-detail/ChatComposer.tsx +21 -0
  143. package/src/screens/chat-detail/ChatDetail.tsx +154 -28
  144. package/src/screens/chat-detail/ChatLinkPreview.tsx +86 -0
  145. package/src/screens/chat-detail/ChatList.tsx +3 -0
  146. package/src/screens/chat-detail/ChatListLegend.tsx +404 -0
  147. package/src/screens/chat-detail/ChatQuickActions.tsx +19 -2
  148. package/src/screens/chat-detail/conversationHeader.utils.ts +52 -0
  149. package/src/screens/chat-detail/index.ts +7 -0
  150. package/src/screens/chat-detail/legend/LegendChatDay.tsx +70 -0
  151. package/src/screens/chat-detail/legend/LegendChatLoadEarlier.tsx +21 -0
  152. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +66 -0
  153. package/src/screens/chat-detail/legend/LegendChatScrollToBottom.tsx +56 -0
  154. package/src/screens/chat-detail/legend/message-types.tsx +149 -0
  155. package/src/screens/chat-detail/messages/ChatMessageBubble.tsx +0 -1
  156. package/src/screens/chat-detail/types.ts +43 -3
  157. package/src/services/apis.ts +1 -1
  158. package/src/services/endpoints.ts +5 -0
  159. package/src/types/common.ts +5 -0
  160. package/src/utils/legendListMessage.ts +102 -0
  161. package/src/utils/url.ts +5 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../../../src/utils/url.ts"],"names":[],"mappings":"AAEA,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEhE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droppii-org/chat-mobile",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Droppii chat mobile",
5
5
  "keywords": [
6
6
  "react-native",
@@ -67,11 +67,13 @@
67
67
  "build:web": "vite build"
68
68
  },
69
69
  "dependencies": {
70
- "@droppii/openim-rn-client-sdk": "git+ssh://git@github.com/droppii/open-im-sdk-reactnative.git#v1.0.0-rc5",
70
+ "@droppii/openim-rn-client-sdk": "git+ssh://git@github.com/droppii/open-im-sdk-reactnative.git#v1.0.0-rc7",
71
+ "@legendapp/list": "^3.0.6",
71
72
  "axios": "^1.16.1",
72
73
  "lodash": "^4.18.1",
73
74
  "react-native-fs": "^2.20.0",
74
75
  "react-native-gifted-chat": "^3.3.3",
76
+ "react-native-permissions": "^5.6.0",
75
77
  "react-native-tab-view": "^4.3.1"
76
78
  },
77
79
  "devDependencies": {
@@ -0,0 +1,49 @@
1
+ export interface FeatureFlags {
2
+ USE_LEGEND_LIST_CHAT: boolean;
3
+ CHAT_LIST_VARIANT: 'gifted' | 'legend';
4
+ ENABLE_PERFORMANCE_METRICS: boolean;
5
+ METRICS_SAMPLE_RATE: number;
6
+ }
7
+
8
+ const DEFAULT_FLAGS: FeatureFlags = {
9
+ USE_LEGEND_LIST_CHAT: false,
10
+ CHAT_LIST_VARIANT: 'legend',
11
+ ENABLE_PERFORMANCE_METRICS: true,
12
+ METRICS_SAMPLE_RATE: 1.0,
13
+ };
14
+
15
+ let flags: FeatureFlags = { ...DEFAULT_FLAGS };
16
+
17
+ export const getFeatureFlag = <K extends keyof FeatureFlags>(
18
+ flag: K
19
+ ): FeatureFlags[K] => {
20
+ return flags[flag];
21
+ };
22
+
23
+ export const setFeatureFlag = <K extends keyof FeatureFlags>(
24
+ flag: K,
25
+ value: FeatureFlags[K]
26
+ ) => {
27
+ flags[flag] = value;
28
+ notifyListeners();
29
+ };
30
+
31
+ export const resetFlags = () => {
32
+ flags = { ...DEFAULT_FLAGS };
33
+ notifyListeners();
34
+ };
35
+
36
+ const listeners = new Set<() => void>();
37
+
38
+ export const onFlagsChange = (callback: () => void) => {
39
+ listeners.add(callback);
40
+ return () => {
41
+ listeners.delete(callback);
42
+ };
43
+ };
44
+
45
+ const notifyListeners = () => {
46
+ listeners.forEach((cb) => cb());
47
+ };
48
+
49
+ export const getAllFlags = (): FeatureFlags => ({ ...flags });
@@ -1,3 +1,8 @@
1
+ export const commonQueryKeys = {
2
+ all: ['common'] as const,
3
+ urlMetadata: (url: string) => ['list', url] as const,
4
+ };
5
+
1
6
  export const conversationQueryKeys = {
2
7
  all: ['conversations'] as const,
3
8
  lists: () => [...conversationQueryKeys.all, 'list'] as const,
@@ -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;
@@ -44,11 +46,13 @@ export const ChatComposer = memo(
44
46
  onSend,
45
47
  onPressAttach,
46
48
  onPressEmoji,
49
+ showQuickActions = true,
47
50
  quickActions,
48
51
  attachmentActions,
49
52
  onQuickActionPress,
50
53
  onAttachmentAction,
51
54
  renderQuickAction,
55
+ renderQuickActions,
52
56
  renderAttachmentAction,
53
57
  attachmentColumns,
54
58
  }: ChatComposerProps) => {
@@ -60,6 +64,13 @@ export const ChatComposer = memo(
60
64
  const [isInputWrapped, setIsInputWrapped] = useState(false);
61
65
  const insets = useSafeAreaInsets();
62
66
 
67
+ const {
68
+ isVisible: showLinkPreview,
69
+ metadata: urlMetadata,
70
+ isLoading: isMetadataLoading,
71
+ dismiss: handleDismissPreview,
72
+ } = useLinkPreview(value);
73
+
63
74
  const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
64
75
  const keyboardHeight = useKeyboardState((state) => state.height);
65
76
  const { progress: keyboardProgress } = useReanimatedKeyboardAnimation();
@@ -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
@@ -1,15 +1,32 @@
1
- import { memo, useState, useCallback } from 'react';
1
+ import { memo, useState, useCallback, useEffect, useMemo } from 'react';
2
2
  import { Platform, StyleSheet } from 'react-native';
3
+
3
4
  import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
4
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
6
  import { KContainer } from '@droppii/libs';
7
+ import { useChatMessages } from '../../hooks/useChatMessages';
8
+ import { useConversation, useConversationStore } from '../../store';
6
9
  import ChatDetailHeader from './ChatDetailHeader';
7
10
  import { ChatList } from './ChatList';
11
+ import { ChatListLegend } from './ChatListLegend';
8
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';
9
24
  import type { ChatDetailProps } from './types';
10
25
 
11
26
  const ChatDetail = memo(
12
27
  ({
28
+ conversationId,
29
+ enabled = true,
13
30
  title,
14
31
  subtitle,
15
32
  avatarUri,
@@ -18,33 +35,108 @@ const ChatDetail = memo(
18
35
  chatCategory,
19
36
  applicationType,
20
37
  showAddMember,
38
+ getSubtitle,
21
39
  onBack,
22
40
  onPressSearch,
23
41
  onPressAddMember,
24
42
  onPressMenu,
25
43
  onPressAvatar,
26
- messages = [],
27
- currentUserId,
28
44
  renderChat,
29
- onLoadEarlier,
30
- isLoading,
31
- isLoadingEarlier,
32
- hasMoreEarlier,
45
+ showQuickActions = true,
46
+ showAttachmentActions = true,
33
47
  quickActions,
48
+ getQuickActions,
34
49
  inputValue,
35
50
  inputPlaceholder,
36
51
  onChangeInput,
37
- onSend,
38
52
  onPressAttach,
39
53
  onPressEmoji,
40
54
  attachmentActions,
41
55
  onQuickActionPress,
42
56
  onAttachmentAction,
43
57
  renderQuickAction,
58
+ renderQuickActions,
44
59
  renderAttachmentAction,
45
60
  attachmentColumns,
46
61
  }: ChatDetailProps) => {
47
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
+ }, []);
48
140
 
49
141
  const handleChangeInput = useCallback(
50
142
  (text: string) => {
@@ -58,19 +150,39 @@ const ChatDetail = memo(
58
150
  );
59
151
 
60
152
  const currentInput = inputValue ?? internalInput;
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
+
61
173
  const insets = useSafeAreaInsets();
62
174
 
63
175
  return (
64
176
  <KContainer.Page flex edges={['bottom']}>
65
177
  <ChatDetailHeader
66
- title={title}
67
- subtitle={subtitle}
68
- avatarUri={avatarUri}
69
- avatarFullName={avatarFullName}
70
- chatType={chatType}
71
- chatCategory={chatCategory}
72
- applicationType={applicationType}
73
- 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}
74
186
  onBack={onBack}
75
187
  onPressSearch={onPressSearch}
76
188
  onPressAddMember={onPressAddMember}
@@ -86,28 +198,42 @@ const ChatDetail = memo(
86
198
  default: 0,
87
199
  })}
88
200
  >
89
- <ChatList
90
- messages={messages}
91
- currentUserId={currentUserId}
92
- renderChat={renderChat}
93
- onLoadEarlier={onLoadEarlier}
94
- isLoading={isLoading}
95
- isLoadingEarlier={isLoadingEarlier}
96
- hasMoreEarlier={hasMoreEarlier}
97
- />
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
+ )}
98
222
 
99
223
  <ChatComposer
100
224
  value={currentInput}
101
225
  placeholder={inputPlaceholder}
102
226
  onChangeText={handleChangeInput}
103
- onSend={onSend}
227
+ onSend={handleSend}
104
228
  onPressAttach={onPressAttach}
105
229
  onPressEmoji={onPressEmoji}
106
- quickActions={quickActions}
107
- attachmentActions={attachmentActions}
230
+ showQuickActions={showQuickActions}
231
+ quickActions={resolvedQuickActions}
232
+ attachmentActions={resolvedAttachmentActions}
108
233
  onQuickActionPress={onQuickActionPress}
109
234
  onAttachmentAction={onAttachmentAction}
110
235
  renderQuickAction={renderQuickAction}
236
+ renderQuickActions={renderQuickActions}
111
237
  renderAttachmentAction={renderAttachmentAction}
112
238
  attachmentColumns={attachmentColumns}
113
239
  />
@@ -0,0 +1,86 @@
1
+ import { memo } from 'react';
2
+ import { ActivityIndicator, StyleSheet } from 'react-native';
3
+ import {
4
+ KContainer,
5
+ KImage,
6
+ KLabel,
7
+ KColors,
8
+ KSpacingValue,
9
+ KDims,
10
+ } from '@droppii/libs';
11
+ import type { IUrlMetadata } from '../../types/common';
12
+
13
+ interface ChatLinkPreviewProps {
14
+ metadata?: IUrlMetadata;
15
+ isLoading?: boolean;
16
+ onDismiss?: () => void;
17
+ }
18
+
19
+ const HEIGHT = 51;
20
+
21
+ export const ChatLinkPreview = memo(
22
+ ({ metadata, isLoading, onDismiss }: ChatLinkPreviewProps) => {
23
+ return (
24
+ <KContainer.View style={styles.container}>
25
+ <KContainer.View style={styles.left}>
26
+ {isLoading ? (
27
+ <ActivityIndicator color={KColors.primary.normal} size={16} />
28
+ ) : metadata?.image ? (
29
+ <KImage.Base uri={metadata?.image} width={HEIGHT} height={HEIGHT} />
30
+ ) : (
31
+ <KImage.VectorIcons
32
+ name="link-alt-o"
33
+ color={KColors.primary.normal}
34
+ />
35
+ )}
36
+ </KContainer.View>
37
+ <KContainer.View style={styles.center}>
38
+ <KLabel.Text typo="TextNmMedium" numberOfLines={1}>
39
+ {metadata?.title}
40
+ </KLabel.Text>
41
+ <KLabel.Text
42
+ typo="TextSmNormal"
43
+ color={KColors.gray.light}
44
+ numberOfLines={1}
45
+ >
46
+ {metadata?.description}
47
+ </KLabel.Text>
48
+ </KContainer.View>
49
+ <KContainer.Touchable onPress={onDismiss} style={styles.right}>
50
+ <KImage.VectorIcons name="close" size={16} />
51
+ </KContainer.Touchable>
52
+ </KContainer.View>
53
+ );
54
+ }
55
+ );
56
+
57
+ ChatLinkPreview.displayName = 'ChatLinkPreview';
58
+
59
+ const styles = StyleSheet.create({
60
+ container: {
61
+ position: 'absolute',
62
+ flexDirection: 'row',
63
+ flex: 1,
64
+ width: KDims.width,
65
+ alignItems: 'center',
66
+ gap: KSpacingValue['0.5rem'],
67
+ backgroundColor: KColors.palette.blue.w25,
68
+ height: HEIGHT,
69
+ top: -HEIGHT,
70
+ zIndex: 10000,
71
+ },
72
+ left: {
73
+ width: HEIGHT,
74
+ height: HEIGHT,
75
+ justifyContent: 'center',
76
+ alignItems: 'center',
77
+ },
78
+ center: {
79
+ flex: 1,
80
+ },
81
+ right: {
82
+ paddingRight: KSpacingValue['0.5rem'],
83
+ justifyContent: 'center',
84
+ alignItems: 'center',
85
+ },
86
+ });
@@ -9,6 +9,7 @@ import {
9
9
  import { KContainer, KColors, KSpacingValue } from '@droppii/libs';
10
10
  import { mapOpenIMMessagesToGiftedChat } from '../../utils/giftedChatMessage';
11
11
  import type { DGiftedChatMessage } from '../../utils/giftedChatMessage';
12
+
12
13
  import type { ChatListProps } from './types';
13
14
  import { ChatDay } from './ChatDay';
14
15
  import { ChatLoadEarlier } from './ChatLoadEarlier';
@@ -24,6 +25,8 @@ export const ChatList = memo(
24
25
  isLoading,
25
26
  isLoadingEarlier,
26
27
  hasMoreEarlier,
28
+ // Note: onLoadNewer, isLoadingNewer, hasMoreNewer are not used in GiftedChat
29
+ // as it has its own pagination mechanism. They're kept for API compatibility.
27
30
  }: ChatListProps) => {
28
31
  const giftedMessages = useMemo(
29
32
  () => mapOpenIMMessagesToGiftedChat(messages),