@droppii-org/chat-mobile 0.2.54 → 0.2.56

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 (40) hide show
  1. package/lib/module/components/messages/imageMessage/index.js +31 -14
  2. package/lib/module/components/messages/imageMessage/index.js.map +1 -1
  3. package/lib/module/components/messages/stickerMessage/index.js +1 -1
  4. package/lib/module/components/messages/stickerMessage/index.js.map +1 -1
  5. package/lib/module/screens/ChatInfo/ChatInfo.js +2 -14
  6. package/lib/module/screens/ChatInfo/ChatInfo.js.map +1 -1
  7. package/lib/module/screens/ChatInfo/ChatInfoProfile.js +14 -19
  8. package/lib/module/screens/ChatInfo/ChatInfoProfile.js.map +1 -1
  9. package/lib/module/screens/MediaView/index.js +7 -3
  10. package/lib/module/screens/MediaView/index.js.map +1 -1
  11. package/lib/module/screens/chat-detail/ChatDetail.js +5 -3
  12. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  13. package/lib/module/screens/chat-detail/ChatListLegend.js +91 -36
  14. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  15. package/lib/typescript/src/components/messages/imageMessage/index.d.ts +12 -0
  16. package/lib/typescript/src/components/messages/imageMessage/index.d.ts.map +1 -1
  17. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts +1 -0
  18. package/lib/typescript/src/components/messages/stickerMessage/index.d.ts.map +1 -1
  19. package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts +1 -1
  20. package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts.map +1 -1
  21. package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts +1 -2
  22. package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts.map +1 -1
  23. package/lib/typescript/src/screens/ChatInfo/types.d.ts +1 -4
  24. package/lib/typescript/src/screens/ChatInfo/types.d.ts.map +1 -1
  25. package/lib/typescript/src/screens/MediaView/index.d.ts.map +1 -1
  26. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  27. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  28. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  29. package/lib/typescript/src/screens/chat-detail/types.d.ts +1 -1
  30. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/components/messages/imageMessage/index.tsx +48 -19
  33. package/src/components/messages/stickerMessage/index.tsx +1 -1
  34. package/src/screens/ChatInfo/ChatInfo.tsx +2 -16
  35. package/src/screens/ChatInfo/ChatInfoProfile.tsx +37 -39
  36. package/src/screens/ChatInfo/types.ts +1 -4
  37. package/src/screens/MediaView/index.tsx +5 -1
  38. package/src/screens/chat-detail/ChatDetail.tsx +6 -3
  39. package/src/screens/chat-detail/ChatListLegend.tsx +125 -51
  40. package/src/screens/chat-detail/types.ts +1 -1
@@ -6,7 +6,6 @@ import {
6
6
  type ConversationItem,
7
7
  } from '@droppii/openim-rn-client-sdk';
8
8
  import { Avatar } from '../../components/Avatar';
9
- import { useGetUsersInfo } from '../../hooks/users/useGetUsersInfo';
10
9
  import { useMuteConversation } from '../../hooks/useMuteConversation';
11
10
  import { trans } from '../../translation';
12
11
  import { StyleSheet } from 'react-native';
@@ -15,51 +14,44 @@ import { NamePrefixIcon } from '../../components/ThreadCard';
15
14
  interface QuickActionProps {
16
15
  iconName: string;
17
16
  label: string;
17
+ background: string;
18
+ iconColor: string;
18
19
  onPress?: () => void;
19
20
  }
20
21
 
21
- const QuickAction = memo(({ iconName, label, onPress }: QuickActionProps) => (
22
- <KContainer.View alignItems paddingH="0.75rem" paddingV="0.5rem">
23
- <KContainer.Touchable
24
- height={40}
25
- width={40}
26
- br={'round'}
27
- background={KColors.palette.gray.w25}
28
- center
29
- onPress={onPress}
30
- >
31
- <KImage.VectorIcons name={iconName} size={18} color={KColors.gray.dark} />
32
- </KContainer.Touchable>
33
- <KLabel.Text
34
- typo="TextSmMedium"
35
- color={KColors.gray.dark}
36
- marginT="0.25rem"
37
- textAlign="center"
38
- >
39
- {label}
40
- </KLabel.Text>
41
- </KContainer.View>
42
- ));
22
+ const QuickAction = memo(
23
+ ({ iconName, label, background, iconColor, onPress }: QuickActionProps) => (
24
+ <KContainer.View alignItems paddingH="0.75rem" paddingV="0.5rem">
25
+ <KContainer.Touchable
26
+ height={40}
27
+ width={40}
28
+ br={'round'}
29
+ background={background}
30
+ center
31
+ onPress={onPress}
32
+ >
33
+ <KImage.VectorIcons name={iconName} size={18} color={iconColor} />
34
+ </KContainer.Touchable>
35
+ <KLabel.Text
36
+ typo="TextSmMedium"
37
+ color={KColors.gray.dark}
38
+ marginT="0.25rem"
39
+ textAlign="center"
40
+ >
41
+ {label}
42
+ </KLabel.Text>
43
+ </KContainer.View>
44
+ )
45
+ );
43
46
 
44
47
  interface ChatInfoProfileProps {
45
48
  conversation?: ConversationItem;
46
- targetUserId?: string;
47
49
  onPressSearch?: () => void;
48
50
  }
49
51
 
50
52
  const ChatInfoProfile = memo(
51
- ({ conversation, targetUserId, onPressSearch }: ChatInfoProfileProps) => {
52
- const { data: usersInfo } = useGetUsersInfo(
53
- targetUserId ? [targetUserId] : []
54
- );
55
-
56
- const { faceURL, showName } = useMemo(() => {
57
- const userInfo = usersInfo?.[0];
58
- return {
59
- faceURL: conversation?.faceURL || userInfo?.faceURL,
60
- showName: conversation?.showName || userInfo?.nickname,
61
- };
62
- }, [conversation, usersInfo]);
53
+ ({ conversation, onPressSearch }: ChatInfoProfileProps) => {
54
+ const { faceURL, showName = '' } = conversation ?? {};
63
55
 
64
56
  const isMuted =
65
57
  conversation != null &&
@@ -98,7 +90,7 @@ const ChatInfoProfile = memo(
98
90
  <Avatar
99
91
  source={faceURL}
100
92
  size="4xlg"
101
- fullName={showName || ''}
93
+ fullName={showName}
102
94
  badge={badge}
103
95
  />
104
96
  <KContainer.View row alignItems marginT="0.5rem">
@@ -108,7 +100,7 @@ const ChatInfoProfile = memo(
108
100
  color={KColors.black}
109
101
  numberOfLines={1}
110
102
  >
111
- {showName || ''}
103
+ {showName}
112
104
  </KLabel.Text>
113
105
  </KContainer.View>
114
106
  </KContainer.View>
@@ -124,12 +116,18 @@ const ChatInfoProfile = memo(
124
116
  iconName="search-o"
125
117
  label={trans('chat:info_search_action')}
126
118
  onPress={onPressSearch}
119
+ background={KColors.palette.gray.w25}
120
+ iconColor={KColors.gray.dark}
127
121
  />
128
122
  <QuickAction
129
- iconName={isMuted ? 'bell-o' : 'bell-off-o'}
123
+ iconName={isMuted ? 'bell-b' : 'bell-off-o'}
130
124
  label={
131
125
  isMuted ? trans('chat:unmute_action') : trans('chat:mute_action')
132
126
  }
127
+ background={
128
+ isMuted ? KColors.primary.normal : KColors.palette.gray.w25
129
+ }
130
+ iconColor={isMuted ? KColors.white : KColors.gray.dark}
133
131
  onPress={handleNotificationPress}
134
132
  />
135
133
  </KContainer.View>
@@ -1,10 +1,7 @@
1
1
  import type { DThreadResourceContentType } from '../../types/chat';
2
2
 
3
3
  export interface ChatInfoProps {
4
- targetConversationId?: string;
5
- targetUserId?: string;
6
- targetGroupId?: string;
7
- targetBotId?: string;
4
+ conversationId?: string;
8
5
  onBack?: () => void;
9
6
  onPressSearch?: () => void;
10
7
  onPressMedia?: (tab: DThreadResourceContentType) => void;
@@ -7,6 +7,7 @@ import {
7
7
  StyleSheet,
8
8
  type ViewToken,
9
9
  StatusBar,
10
+ useWindowDimensions,
10
11
  } from 'react-native';
11
12
  import {
12
13
  LegendList,
@@ -65,6 +66,8 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
65
66
  const [isScrolling, setIsScrolling] = useState(false);
66
67
  const [isDownloading, setIsDownloading] = useState(false);
67
68
 
69
+ const { height } = useWindowDimensions();
70
+
68
71
  const dismiss = useCallback(() => {
69
72
  onDismiss?.();
70
73
  }, [onDismiss]);
@@ -153,6 +156,7 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
153
156
  url={item?.url || ''}
154
157
  isActive={itemIndex === index && !isScrolling}
155
158
  showCustomControls={false}
159
+ height={height}
156
160
  />
157
161
  </KContainer.VisibleView>
158
162
  <KContainer.VisibleView visible={!item.isVideo}>
@@ -167,7 +171,7 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
167
171
  </KContainer.VisibleView>
168
172
  </KContainer.Touchable>
169
173
  ),
170
- [index, isScrolling]
174
+ [index, isScrolling, height]
171
175
  );
172
176
 
173
177
  const renderFooter = useMemo(() => {
@@ -46,7 +46,6 @@ const ChatDetail = memo(
46
46
  onPressSearch,
47
47
  onPressAddMember,
48
48
  onPressMenu,
49
- onPressAvatar,
50
49
  onPressUrl,
51
50
  customMessageItemFactory,
52
51
  pannelActions,
@@ -149,6 +148,10 @@ const ChatDetail = memo(
149
148
  });
150
149
  }, [currentUser, unpinMessage, customMessageItemFactory]);
151
150
 
151
+ const handlePressMenu = useCallback(() => {
152
+ onPressMenu?.(conversationId);
153
+ }, [conversationId, onPressMenu]);
154
+
152
155
  return (
153
156
  <KContainer.Page flex edges={['bottom']}>
154
157
  <ChatDetailHeader
@@ -158,8 +161,8 @@ const ChatDetail = memo(
158
161
  onBack={onBack ?? handleBackDefault}
159
162
  onPressSearch={onPressSearch}
160
163
  onPressAddMember={onPressAddMember}
161
- onPressMenu={onPressMenu}
162
- onPressAvatar={onPressAvatar}
164
+ onPressMenu={handlePressMenu}
165
+ onPressAvatar={handlePressMenu}
163
166
  />
164
167
 
165
168
  <PinnedMessageBar
@@ -7,7 +7,12 @@ import {
7
7
  useState,
8
8
  type RefObject,
9
9
  } from 'react';
10
- import { DeviceEventEmitter, StyleSheet, type View } from 'react-native';
10
+ import {
11
+ DeviceEventEmitter,
12
+ StyleSheet,
13
+ useWindowDimensions,
14
+ type View,
15
+ } from 'react-native';
11
16
  import Animated, {
12
17
  useSharedValue,
13
18
  useAnimatedStyle,
@@ -32,9 +37,20 @@ import { ChatEvent, type ScrollToMessagePayload } from '../../types/events';
32
37
  import { useMessageStore } from '../../store/message';
33
38
  import { useChatContext } from '../../context';
34
39
  import { messageStyles } from './legend/messageTypes';
40
+ import { MessageType } from '@droppii/openim-rn-client-sdk';
35
41
  import type { MessageItem } from '@droppii/openim-rn-client-sdk';
36
42
  import { useUserStore } from '../../store';
37
43
  import { ChatMessageAPI } from '../../services';
44
+ import { resolveImageDisplayDims } from '../../components/messages/imageMessage';
45
+ import { STICKER_SIZE } from '../../components/messages/stickerMessage';
46
+ import ChatImageDimens, {
47
+ CHAT_IMAGE_VIEW_SOURCE,
48
+ } from '../../utils/chatImageDimens';
49
+
50
+ // Row wrapper (`messageRowLeft`/`messageRowRight` in `components/messages/styles.ts`)
51
+ // adds only a bottom margin — no vertical padding — so a fixed-size item's true
52
+ // rendered height is `content height + this margin`.
53
+ const ROW_MARGIN_BOTTOM = KSpacingValue['0.25rem'];
38
54
 
39
55
  interface ChatListLegendInternalProps extends ChatListProps {
40
56
  listRef: RefObject<LegendListRef | null>;
@@ -66,6 +82,7 @@ export const ChatListLegend = memo(
66
82
  keyboardOffset,
67
83
  }: ChatListLegendInternalProps) => {
68
84
  const currentUserId = useUserStore((s) => s.user?.userID);
85
+ const windowWidth = useWindowDimensions().width;
69
86
  // Precompute all message data in single pass (O(n))
70
87
  const messageDataMap = useMemo(
71
88
  () => precomputeLegendListData(messages),
@@ -85,39 +102,12 @@ export const ChatListLegend = memo(
85
102
  const buttonOpacity = useSharedValue(0);
86
103
  const isAtBottomRef = useRef(true);
87
104
  const scrollDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
88
- const itemHeightsRef = useRef<Map<string, number>>(new Map());
89
- const avgItemHeightRef = useRef(120);
90
- const heightDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
91
105
  const rowRefsMap = useRef<Map<string, View | null>>(new Map());
92
106
  const [showBadge, setShowBadge] = useState(false);
93
107
 
94
108
  const { applicationType } = useChatContext();
95
109
  const searchMessages = useMessageStore((s) => s.searchMessages);
96
110
 
97
- const handleItemLayout = useCallback(
98
- (messageId: string, height: number) => {
99
- itemHeightsRef.current.set(messageId, height);
100
-
101
- // Debounce average height calculation (batch updates)
102
- if (heightDebounceRef.current) {
103
- clearTimeout(heightDebounceRef.current);
104
- }
105
-
106
- heightDebounceRef.current = setTimeout(() => {
107
- if (itemHeightsRef.current.size > 0) {
108
- const total = Array.from(itemHeightsRef.current.values()).reduce(
109
- (a, b) => a + b,
110
- 0
111
- );
112
- avgItemHeightRef.current = Math.round(
113
- total / itemHeightsRef.current.size
114
- );
115
- }
116
- }, 100); // Batch updates every 100ms during load
117
- },
118
- []
119
- );
120
-
121
111
  const handleMessageLongPress = useCallback(
122
112
  (messageId: string, message: MessageItem) => {
123
113
  const node = rowRefsMap.current.get(messageId);
@@ -181,9 +171,6 @@ export const ChatListLegend = memo(
181
171
  ? messageStyles.messageRowRight
182
172
  : messageStyles.messageRowLeft
183
173
  }
184
- onLayout={(e) =>
185
- handleItemLayout(messageId, e.nativeEvent.layout.height)
186
- }
187
174
  >
188
175
  <LegendChatMessage
189
176
  message={message}
@@ -234,30 +221,118 @@ export const ChatListLegend = memo(
234
221
  onResetFlow,
235
222
  onRunFlow,
236
223
  conversationFlow?.flowCurrentStep,
237
- handleItemLayout,
238
224
  retryMessage,
239
225
  ]
240
226
  );
241
227
 
242
- const maintainScrollConfig = useMemo(
243
- () => ({
244
- animated: false,
245
- on: {
246
- dataChange: !moreOldLoading,
247
- layout: !moreOldLoading,
248
- // Must be disabled while prepending older history; otherwise re-measuring
249
- // the freshly prepended items snaps the list back to the bottom.
250
- itemLayout: !moreOldLoading,
251
- },
252
- }),
253
- [moreOldLoading]
254
- );
228
+ // const maintainScrollConfig = useMemo(
229
+ // () => ({
230
+ // animated: false,
231
+ // on: {
232
+ // dataChange: !moreOldLoading,
233
+ // layout: !moreOldLoading,
234
+ // // Must be disabled while prepending older history; otherwise re-measuring
235
+ // // the freshly prepended items snaps the list back to the bottom.
236
+ // itemLayout: !moreOldLoading,
237
+ // },
238
+ // }),
239
+ // [moreOldLoading]
240
+ // );
255
241
 
256
242
  // Stable config so the native maintainVisibleContentPosition prop isn't toggled
257
243
  // mid-prepend (toggling forces an extra layout recompute). `data` anchoring keeps
258
244
  // the viewport pinned to the item being read when older messages are added on top.
259
245
  const maintainVisiblePositionConfig = useMemo(() => ({ data: true }), []);
260
246
 
247
+ // Buckets averaging (`averageSizes[itemType]`, internal to LegendList) per
248
+ // content type, so a batch of images landing early doesn't pollute the
249
+ // estimate used for still-unmeasured text rows and vice versa. Items
250
+ // rendered via `customMessageItemFactory` (host apps can override ANY
251
+ // content type with an arbitrary custom node — see `LegendChatMessage`'s
252
+ // `customNode` short-circuit, checked before its own contentType switch)
253
+ // get their own bucket per content type, so a custom node's real measured
254
+ // height never pollutes the native component's average for that same type.
255
+ const getItemType = useCallback(
256
+ (item: MessageItem) => {
257
+ const isCustom = !!customMessageItemFactory?.(item);
258
+ return `${item?.contentType ?? 'default'}${isCustom ? '-custom' : ''}`;
259
+ },
260
+ [customMessageItemFactory]
261
+ );
262
+
263
+ // Exact size for content types that are knowable ahead of render — see
264
+ // `plans/260728-1400-chatlist-legend-fixed-item-size/plan.md`. Returns
265
+ // `undefined` (defer to dynamic measurement) for anything whose size can't
266
+ // be predicted, AND for any item carrying extra chrome this function
267
+ // doesn't account for (day-divider above it, trailing flow-input-buttons
268
+ // row) — `getFixedItemSize` describes the ENTIRE item LegendList renders,
269
+ // and once it resolves a number that item is never measured again, so
270
+ // guessing wrong here is a permanent regression, not just an approximation.
271
+ const getFixedItemSize = useCallback(
272
+ (item: MessageItem, index: number): number | undefined => {
273
+ const message = item;
274
+ const messageId =
275
+ message.clientMsgID ||
276
+ message.serverMsgID ||
277
+ `temp-${message.sendTime || message.createTime}-${index}`;
278
+
279
+ if (messageDataMap.get(messageId)?.dayStart) return undefined;
280
+
281
+ // Resolve the content-type candidate FIRST — for every type other
282
+ // than these three the result is `undefined` regardless of anything
283
+ // else, so bail before paying for a `customMessageItemFactory` call
284
+ // (text/quote/link/file/merge/revoke are the overwhelming majority of
285
+ // messages in a real chat — skip the extra call for all of them).
286
+ let candidateSize: number;
287
+ switch (message.contentType) {
288
+ case MessageType.PictureMessage:
289
+ candidateSize =
290
+ resolveImageDisplayDims(message, windowWidth).height +
291
+ ROW_MARGIN_BOTTOM;
292
+ break;
293
+ case MessageType.VideoMessage:
294
+ candidateSize =
295
+ ChatImageDimens.getDimensions(
296
+ CHAT_IMAGE_VIEW_SOURCE.MessageListThumbnail
297
+ ).height + ROW_MARGIN_BOTTOM;
298
+ break;
299
+ case MessageType.StickerMessage:
300
+ candidateSize = STICKER_SIZE + ROW_MARGIN_BOTTOM;
301
+ break;
302
+ default:
303
+ return undefined;
304
+ }
305
+
306
+ // `customMessageItemFactory` can override rendering for ANY content
307
+ // type with an arbitrary node (`LegendChatMessage` checks it first,
308
+ // before its own contentType switch) — its real height is unknowable
309
+ // here, so defer to dynamic measurement when it intercepts. Only
310
+ // checked once we know this item would otherwise be fixed-sizeable.
311
+ if (customMessageItemFactory?.(message)) return undefined;
312
+
313
+ const isOutgoing = message.sendID === currentUserId;
314
+ const isLastIncoming = !isOutgoing && messageId === lastMessageId;
315
+ if (
316
+ isLastIncoming &&
317
+ isBotConversation &&
318
+ getFlowInputButtonsNode(conversationFlow?.flowCurrentStep)
319
+ ) {
320
+ return undefined;
321
+ }
322
+
323
+ return candidateSize;
324
+ },
325
+ [
326
+ messageDataMap,
327
+ customMessageItemFactory,
328
+ currentUserId,
329
+ lastMessageId,
330
+ isBotConversation,
331
+ conversationFlow?.flowCurrentStep,
332
+ windowWidth,
333
+ ]
334
+ );
335
+
261
336
  const handleStartReached = useCallback(() => {
262
337
  if (__DEV__) {
263
338
  console.log('[LegendList] onStartReached triggered', {
@@ -320,15 +395,12 @@ export const ChatListLegend = memo(
320
395
  pointerEvents: buttonOpacity.value === 0 ? 'none' : 'auto',
321
396
  }));
322
397
 
323
- // Cleanup debounce timeouts on unmount
398
+ // Cleanup debounce timeout on unmount
324
399
  useEffect(() => {
325
400
  return () => {
326
401
  if (scrollDebounceRef.current) {
327
402
  clearTimeout(scrollDebounceRef.current);
328
403
  }
329
- if (heightDebounceRef.current) {
330
- clearTimeout(heightDebounceRef.current);
331
- }
332
404
  };
333
405
  }, []);
334
406
 
@@ -402,13 +474,15 @@ export const ChatListLegend = memo(
402
474
  ref={listRef}
403
475
  data={messages}
404
476
  renderItem={renderItem}
405
- estimatedItemSize={avgItemHeightRef.current}
477
+ estimatedItemSize={100}
478
+ getItemType={getItemType}
479
+ getFixedItemSize={getFixedItemSize}
406
480
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
407
481
  recycleItems={false}
408
482
  extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}`}
409
483
  alignItemsAtEnd
410
484
  maintainVisibleContentPosition={maintainVisiblePositionConfig}
411
- maintainScrollAtEnd={maintainScrollConfig}
485
+ maintainScrollAtEnd={true}
412
486
  freeze={freeze}
413
487
  keyboardOffset={keyboardOffset}
414
488
  scrollEventThrottle={16}
@@ -42,7 +42,7 @@ export interface ChatDetailHeaderProps {
42
42
  onBack?: () => void;
43
43
  onPressSearch?: () => void;
44
44
  onPressAddMember?: () => void;
45
- onPressMenu?: () => void;
45
+ onPressMenu?: (conversationId?: string) => void;
46
46
  onPressAvatar?: () => void;
47
47
  conversationId: string;
48
48
  targetUserId?: string;