@droppii-org/chat-mobile 0.2.88 → 0.2.90

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 (68) hide show
  1. package/lib/module/components/ThreadCard/ThreadCard.js +0 -1
  2. package/lib/module/components/ThreadCard/ThreadCard.js.map +1 -1
  3. package/lib/module/components/messages/LinkifiedText.js +34 -0
  4. package/lib/module/components/messages/LinkifiedText.js.map +1 -0
  5. package/lib/module/components/messages/quotedMessage/index.js +14 -2
  6. package/lib/module/components/messages/quotedMessage/index.js.map +1 -1
  7. package/lib/module/components/messages/textMessage/index.js +15 -22
  8. package/lib/module/components/messages/textMessage/index.js.map +1 -1
  9. package/lib/module/core/useChatListener.js +2 -2
  10. package/lib/module/core/useChatListener.js.map +1 -1
  11. package/lib/module/hooks/message/useMessages.js +6 -0
  12. package/lib/module/hooks/message/useMessages.js.map +1 -1
  13. package/lib/module/hooks/message/useSendMessage.js +10 -3
  14. package/lib/module/hooks/message/useSendMessage.js.map +1 -1
  15. package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js.map +1 -1
  16. package/lib/module/screens/chat-detail/ChatDetail.js +2 -1
  17. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  18. package/lib/module/screens/chat-detail/ChatListLegend.js +235 -123
  19. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  20. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +4 -1
  21. package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
  22. package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js +3 -1
  23. package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js.map +1 -1
  24. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +11 -9
  25. package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
  26. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +37 -4
  27. package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
  28. package/lib/module/services/resourceCenter.js +45 -22
  29. package/lib/module/services/resourceCenter.js.map +1 -1
  30. package/lib/module/store/message.js +8 -1
  31. package/lib/module/store/message.js.map +1 -1
  32. package/lib/typescript/src/components/ThreadCard/ThreadCard.d.ts.map +1 -1
  33. package/lib/typescript/src/components/messages/LinkifiedText.d.ts +11 -0
  34. package/lib/typescript/src/components/messages/LinkifiedText.d.ts.map +1 -0
  35. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts +1 -0
  36. package/lib/typescript/src/components/messages/quotedMessage/index.d.ts.map +1 -1
  37. package/lib/typescript/src/components/messages/textMessage/index.d.ts.map +1 -1
  38. package/lib/typescript/src/hooks/message/useMessages.d.ts.map +1 -1
  39. package/lib/typescript/src/hooks/message/useSendMessage.d.ts.map +1 -1
  40. package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts.map +1 -1
  41. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  42. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
  43. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  44. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
  45. package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheetBackup.d.ts.map +1 -1
  46. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +5 -3
  47. package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
  48. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts +2 -0
  49. package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
  50. package/lib/typescript/src/services/resourceCenter.d.ts.map +1 -1
  51. package/lib/typescript/src/store/message.d.ts.map +1 -1
  52. package/package.json +2 -2
  53. package/src/components/ThreadCard/ThreadCard.tsx +0 -2
  54. package/src/components/messages/LinkifiedText.tsx +37 -0
  55. package/src/components/messages/quotedMessage/index.tsx +18 -2
  56. package/src/components/messages/textMessage/index.tsx +18 -27
  57. package/src/core/useChatListener.ts +2 -2
  58. package/src/hooks/message/useMessages.ts +6 -0
  59. package/src/hooks/message/useSendMessage.ts +7 -0
  60. package/src/screens/ResourceCenter/ResourceCenterTabScene.tsx +0 -1
  61. package/src/screens/chat-detail/ChatDetail.tsx +1 -0
  62. package/src/screens/chat-detail/ChatListLegend.tsx +236 -136
  63. package/src/screens/chat-detail/PinnedMessagesSheet.tsx +6 -1
  64. package/src/screens/chat-detail/PinnedMessagesSheetBackup.tsx +3 -1
  65. package/src/screens/chat-detail/legend/LegendChatMessage.tsx +16 -11
  66. package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +47 -2
  67. package/src/services/resourceCenter.ts +77 -36
  68. package/src/store/message.ts +7 -1
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  forwardRef,
3
3
  useCallback,
4
+ useEffect,
4
5
  useImperativeHandle,
5
6
  useMemo,
7
+ useRef,
6
8
  useState,
7
9
  } from 'react';
8
10
  import { Modal, Pressable, StyleSheet } from 'react-native';
@@ -18,6 +20,7 @@ import {
18
20
  import type { MeasuredRect, MessageToolboxRef } from './types';
19
21
  import { MessageToolboxWrapper } from './MessageToolboxWrapper';
20
22
  import type { ReactNode } from 'react';
23
+ import type { SharedValue } from 'react-native-reanimated';
21
24
  import { useSendMessage } from '../../../hooks/message/useSendMessage';
22
25
  import { useChatDetailContext } from '../../../context';
23
26
  import {
@@ -26,11 +29,25 @@ import {
26
29
  } from '@droppii/openim-rn-client-sdk';
27
30
  import { useHasGroupPermission } from '../../../hooks/useGroupPermission';
28
31
 
32
+ // Android only: this Modal's native window is added/removed asynchronously
33
+ // relative to its React state, and each of those transitions makes
34
+ // react-native-keyboard-controller's resize listener misfire a spurious
35
+ // zero-height "keyboard" event a short beat later — confirmed via adb logcat,
36
+ // the close-triggered one lands ~30-70ms after `hide()` runs. `freeze` is
37
+ // held past that window (not just released synchronously in `hide()`) so the
38
+ // delayed spurious event is still suppressed instead of replaying a stale
39
+ // scrollTo. See `show`/`hide` below for the observed sequence.
40
+ const UNFREEZE_DELAY_MS = 300;
41
+
29
42
  interface MessageToolboxOverlayProps {
30
43
  currentUserId?: string;
31
44
  customMessageItemFactory?: (item: MessageItem) => ReactNode;
32
45
  onPinMessage?: (message: MessageItem) => void;
33
46
  onUnpinMessage?: (message: MessageItem) => void;
47
+ // Same SharedValue KeyboardAwareLegendList uses to suspend its own
48
+ // keyboard-driven scroll adjustments (see MessageToolboxOverlay's
49
+ // show/hide below for why this Modal needs it).
50
+ freeze?: SharedValue<boolean>;
34
51
  }
35
52
 
36
53
  export const MessageToolboxOverlay = forwardRef<
@@ -43,6 +60,7 @@ export const MessageToolboxOverlay = forwardRef<
43
60
  customMessageItemFactory,
44
61
  onPinMessage,
45
62
  onUnpinMessage,
63
+ freeze,
46
64
  },
47
65
  ref
48
66
  ) => {
@@ -56,12 +74,31 @@ export const MessageToolboxOverlay = forwardRef<
56
74
  );
57
75
  const { retryMessage, revokeMessage } = useSendMessage();
58
76
  const isPinAllowed = useHasGroupPermission(GroupPermission.PinMessage);
77
+ const unfreezeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
78
+ null
79
+ );
80
+
81
+ const clearPendingUnfreeze = useCallback(() => {
82
+ if (unfreezeTimeoutRef.current) {
83
+ clearTimeout(unfreezeTimeoutRef.current);
84
+ unfreezeTimeoutRef.current = null;
85
+ }
86
+ }, []);
87
+
88
+ useEffect(() => clearPendingUnfreeze, [clearPendingUnfreeze]);
59
89
 
60
90
  const hide = useCallback(() => {
61
91
  setVisible(false);
62
92
  setMessage(undefined);
63
93
  setRect(null);
64
- }, []);
94
+ if (freeze) {
95
+ clearPendingUnfreeze();
96
+ unfreezeTimeoutRef.current = setTimeout(() => {
97
+ freeze.value = false;
98
+ unfreezeTimeoutRef.current = null;
99
+ }, UNFREEZE_DELAY_MS);
100
+ }
101
+ }, [freeze, clearPendingUnfreeze]);
65
102
 
66
103
  const show = useCallback(
67
104
  (msg: MessageItem, msgRect: MeasuredRect) => {
@@ -72,11 +109,18 @@ export const MessageToolboxOverlay = forwardRef<
72
109
 
73
110
  if (toolboxNotAvailable) return;
74
111
 
112
+ if (freeze) {
113
+ // Cancel a still-pending delayed unfreeze from a just-closed
114
+ // toolbox so it can't fire mid-way through this new one.
115
+ clearPendingUnfreeze();
116
+ freeze.value = true;
117
+ }
118
+
75
119
  setMessage(msg);
76
120
  setRect(msgRect);
77
121
  setVisible(true);
78
122
  },
79
- [currentUserId]
123
+ [currentUserId, freeze, clearPendingUnfreeze]
80
124
  );
81
125
 
82
126
  useImperativeHandle(ref, () => ({ show, hide }), [show, hide]);
@@ -170,6 +214,7 @@ export const MessageToolboxOverlay = forwardRef<
170
214
  message={
171
215
  <LegendChatMessage
172
216
  message={message}
217
+ messageId={message.clientMsgID || message.serverMsgID || ''}
173
218
  isOutgoing={isOutgoing}
174
219
  createdAtTime={message.sendTime || message.createTime}
175
220
  customMessageItemFactory={customMessageItemFactory}
@@ -60,13 +60,49 @@ const safeJsonParse = <T>(raw: string | undefined): T | undefined => {
60
60
  }
61
61
  };
62
62
 
63
+ type LinkFields = Pick<
64
+ DThreadResource,
65
+ 'linkUrl' | 'linkTitle' | 'linkDescription' | 'linkImageUrl' | 'linkDomain'
66
+ >;
67
+
68
+ // The Link tab's endpoint returns any message that contains a URL, not just
69
+ // dedicated URL_TEXT (160) messages — @mention text (106) and quoted replies
70
+ // (114) with a link show up here too, still carrying `content.urls` / `ex.urlMetadata`.
71
+ // A single message can carry MULTIPLE urls in `content.urls` (e.g. "check
72
+ // https://a.com and https://b.com") — BE only splits multi-url messages into
73
+ // separate rows for dedicated URL_TEXT (160); for other content types it stays
74
+ // one row, so every url must be turned into its own item here. `ex.urlMetadata`
75
+ // only ever describes one link (the one it matches by exact url), so the rest
76
+ // render as domain-only cards (no title/image) — the UI already supports that.
77
+ const parseLinkItems = (
78
+ chatLog: DThreadResourceRawRow['chatLog']
79
+ ): LinkFields[] => {
80
+ const ex = safeJsonParse<RawUrlTextEx>(chatLog.ex);
81
+ const content = safeJsonParse<RawUrlTextContent>(chatLog.content);
82
+ const metadata = ex?.urlMetadata;
83
+ const urls = content?.urls?.length
84
+ ? content.urls
85
+ : metadata?.url
86
+ ? [metadata.url]
87
+ : [];
88
+ return urls.map((linkUrl) => {
89
+ const matchesMetadata = !!metadata?.url && metadata.url === linkUrl;
90
+ return {
91
+ linkUrl,
92
+ linkTitle: matchesMetadata ? metadata?.title : undefined,
93
+ linkDescription: matchesMetadata ? metadata?.description : undefined,
94
+ linkImageUrl: matchesMetadata ? metadata?.image : undefined,
95
+ linkDomain: extractDomain(linkUrl),
96
+ };
97
+ });
98
+ };
99
+
63
100
  const parseThreadResourceItem = (
64
101
  row: DThreadResourceRawRow
65
- ): DThreadResource => {
102
+ ): DThreadResource[] => {
66
103
  const { chatLog } = row;
67
104
  const contentType = chatLog.contentType as DThreadResourceContentType;
68
- const base: DThreadResource = {
69
- id: chatLog.clientMsgID,
105
+ const base = {
70
106
  contentType,
71
107
  isRevoked: row.isRevoked,
72
108
  createdAt: chatLog.sendTime,
@@ -75,47 +111,52 @@ const parseThreadResourceItem = (
75
111
  switch (contentType) {
76
112
  case DThreadResourceContentType.IMAGE: {
77
113
  const content = safeJsonParse<RawPictureContent>(chatLog.content);
78
- return {
79
- ...base,
80
- sourceUrl: content?.bigPicture?.url,
81
- thumbnailUrl: content?.snapshotPicture?.url,
82
- };
114
+ return [
115
+ {
116
+ id: chatLog.clientMsgID,
117
+ ...base,
118
+ sourceUrl: content?.bigPicture?.url,
119
+ thumbnailUrl: content?.snapshotPicture?.url,
120
+ },
121
+ ];
83
122
  }
84
123
  case DThreadResourceContentType.VIDEO: {
85
124
  const content = safeJsonParse<RawVideoContent>(chatLog.content);
86
- return {
87
- ...base,
88
- sourceUrl: content?.videoUrl,
89
- thumbnailUrl: content?.snapshotUrl,
90
- duration: content?.duration,
91
- };
125
+ return [
126
+ {
127
+ id: chatLog.clientMsgID,
128
+ ...base,
129
+ sourceUrl: content?.videoUrl,
130
+ thumbnailUrl: content?.snapshotUrl,
131
+ duration: content?.duration,
132
+ },
133
+ ];
92
134
  }
93
135
  case DThreadResourceContentType.FILE: {
94
136
  const content = safeJsonParse<RawFileContent>(chatLog.content);
95
- return {
96
- ...base,
97
- sourceUrl: content?.sourceUrl,
98
- fileName: content?.fileName,
99
- fileSize: content?.fileSize,
100
- mimeType: content?.fileType,
101
- };
137
+ return [
138
+ {
139
+ id: chatLog.clientMsgID,
140
+ ...base,
141
+ sourceUrl: content?.sourceUrl,
142
+ fileName: content?.fileName,
143
+ fileSize: content?.fileSize,
144
+ mimeType: content?.fileType,
145
+ },
146
+ ];
102
147
  }
103
- case DThreadResourceContentType.URL_TEXT: {
104
- const ex = safeJsonParse<RawUrlTextEx>(chatLog.ex);
105
- const content = safeJsonParse<RawUrlTextContent>(chatLog.content);
106
- const metadata = ex?.urlMetadata;
107
- const linkUrl = metadata?.url ?? content?.urls?.[0];
108
- return {
148
+ default: {
149
+ const linkItems = parseLinkItems(chatLog);
150
+ if (!linkItems.length) return [{ id: chatLog.clientMsgID, ...base }];
151
+ return linkItems.map((fields, index) => ({
152
+ id:
153
+ linkItems.length > 1
154
+ ? `${chatLog.clientMsgID}_${index}`
155
+ : chatLog.clientMsgID,
109
156
  ...base,
110
- linkUrl,
111
- linkTitle: metadata?.title,
112
- linkDescription: metadata?.description,
113
- linkImageUrl: metadata?.image,
114
- linkDomain: linkUrl ? extractDomain(linkUrl) : undefined,
115
- };
157
+ ...fields,
158
+ }));
116
159
  }
117
- default:
118
- return base;
119
160
  }
120
161
  };
121
162
 
@@ -127,7 +168,7 @@ export namespace ResourceCenterService {
127
168
  BaseResponse<DThreadResourceListResponse>
128
169
  >(getApiEndpoint('messagesResources'), req);
129
170
  const rows = res?.data?.data ?? [];
130
- const items = rows.map(parseThreadResourceItem);
171
+ const items = rows.flatMap(parseThreadResourceItem);
131
172
  // `pageable.totalPages`/`totalElements` come back as `0` on this endpoint even
132
173
  // when rows are present (confirmed against staging) — unreliable, don't use it.
133
174
  // Item-count-vs-pageSize is the only trustworthy `hasMore` signal available.
@@ -14,7 +14,13 @@ import type {
14
14
  import { mergeMessages } from '../utils/message';
15
15
  import { useConversationStore } from './conversation';
16
16
 
17
- const PAGE_SIZE = 20;
17
+ // Smaller than a typical "load more" page size elsewhere in the app on
18
+ // purpose: each page becomes a single prepend batch in ChatListLegend, and a
19
+ // smaller batch means fewer newly-inserted rows still unmeasured by the time
20
+ // native layout catches up — shrinks both the size and duration of the
21
+ // visible settle right after a history-prepend flush (see
22
+ // project_chatlistlegend_scroll_debugging memory for the full context).
23
+ const PAGE_SIZE = 15;
18
24
 
19
25
  export const useMessageStore = create<MessageStore>()((set, get) => ({
20
26
  messages: [],