@droppii-org/chat-mobile 0.2.89 → 0.2.91
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.
- package/lib/module/context/ChatContext.js +5 -3
- package/lib/module/context/ChatContext.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +64 -5
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +93 -123
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js +4 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheet.js.map +1 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js +3 -1
- package/lib/module/screens/chat-detail/PinnedMessagesSheetBackup.js.map +1 -1
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js +5 -4
- package/lib/module/screens/chat-detail/legend/LegendChatMessage.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js +18 -7
- package/lib/module/screens/chat-detail/messageToolbox/MessageToolboxOverlay.js.map +1 -1
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js +3 -5
- package/lib/module/screens/chat-detail/messageToolbox/messageToolbox.utils.js.map +1 -1
- package/lib/module/services/apis.js +2 -1
- package/lib/module/services/apis.js.map +1 -1
- package/lib/module/store/message.js +1 -1
- package/lib/module/store/message.js.map +1 -1
- package/lib/module/store/user.js +6 -0
- package/lib/module/store/user.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +1 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/chat.js.map +1 -1
- package/lib/typescript/src/context/ChatContext.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +2 -2
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheet.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/PinnedMessagesSheetBackup.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts +5 -3
- package/lib/typescript/src/screens/chat-detail/legend/LegendChatMessage.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts +2 -0
- package/lib/typescript/src/screens/chat-detail/messageToolbox/messageToolbox.utils.d.ts.map +1 -1
- package/lib/typescript/src/services/apis.d.ts.map +1 -1
- package/lib/typescript/src/store/user.d.ts +1 -0
- package/lib/typescript/src/store/user.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +4 -0
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/context/ChatContext.tsx +3 -0
- package/src/index.tsx +1 -0
- package/src/screens/chat-detail/ChatDetail.tsx +63 -5
- package/src/screens/chat-detail/ChatListLegend.tsx +94 -156
- package/src/screens/chat-detail/PinnedMessagesSheet.tsx +6 -1
- package/src/screens/chat-detail/PinnedMessagesSheetBackup.tsx +3 -1
- package/src/screens/chat-detail/legend/LegendChatMessage.tsx +9 -6
- package/src/screens/chat-detail/messageToolbox/MessageToolboxOverlay.tsx +20 -7
- package/src/screens/chat-detail/messageToolbox/messageToolbox.utils.ts +4 -4
- package/src/services/apis.ts +2 -1
- package/src/store/message.ts +1 -1
- package/src/store/user.ts +7 -0
- package/src/translation/resources/i18n.ts +2 -0
- package/src/types/chat.ts +14 -0
|
@@ -76,12 +76,13 @@ import ChatImageDimens, {
|
|
|
76
76
|
const ROW_MARGIN_CLUSTER_END = KSpacingValue['0.5rem'];
|
|
77
77
|
const ROW_MARGIN_IN_CLUSTER = KSpacingValue['0.25rem'];
|
|
78
78
|
|
|
79
|
-
// Tags every __DEV__ diagnostic log below with the platform, so logs from an
|
|
80
|
-
// iOS and an Android run can be told apart when compared side by side.
|
|
81
|
-
const LOG_TAG = `[ChatListLegend:${Platform.OS}]`;
|
|
82
|
-
|
|
83
79
|
const BUTTON_FADE_CONFIG = { duration: 200, easing: Easing.out(Easing.ease) };
|
|
84
80
|
|
|
81
|
+
// Long enough to swallow Android's willShow→willHide→willShow flapping
|
|
82
|
+
// (~1.3s period, measured), short enough that a real keyboard close still
|
|
83
|
+
// settles within one animation.
|
|
84
|
+
const KEYBOARD_HIDE_DEBOUNCE_MS = 300;
|
|
85
|
+
|
|
85
86
|
const getRowMarginBottom = (position: GroupPosition): number =>
|
|
86
87
|
position === 'last' || position === 'solo'
|
|
87
88
|
? ROW_MARGIN_CLUSTER_END
|
|
@@ -142,31 +143,16 @@ export const ChatListLegend = memo(
|
|
|
142
143
|
const last = messages[messages.length - 1];
|
|
143
144
|
return last?.clientMsgID || last?.serverMsgID || null;
|
|
144
145
|
}, [messages]);
|
|
145
|
-
// Stopgap for the keyboard-open blank-gap bug (unresolved upstream —
|
|
146
|
-
// see project_chatlistlegend_scroll_debugging memory): below this
|
|
147
|
-
// threshold, disable `alignItemsAtEnd` and let plain CSS `flex-end`
|
|
148
|
-
// anchor short content to the bottom instead. Known trade-off (same
|
|
149
|
-
// memory, bug #17/#19): disabling `alignItemsAtEnd` removes the extra
|
|
150
|
-
// scrollable room `clampScrollOffset` needs, so sending a message while
|
|
151
|
-
// the keyboard is open on one of these same short conversations can
|
|
152
|
-
// land the new message away from the composer instead of scrolling to
|
|
153
|
-
// it — verify that scenario specifically after any change here.
|
|
154
|
-
const isVeryShortConversation = messages.length < 15;
|
|
155
146
|
const { mutate: runFlow } = useRunFlow();
|
|
156
147
|
const { mutate: resetFlow } = useResetFlow();
|
|
157
148
|
|
|
158
149
|
const buttonOpacity = useSharedValue(0);
|
|
159
150
|
const isAtBottomRef = useRef(true);
|
|
160
151
|
const scrollDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// `handleScroll` below to auto-detect the known issue where that native
|
|
166
|
-
// inset silently fails to apply (confirmed via manual trace: contentOffset
|
|
167
|
-
// scrolls correctly but contentInset stays {bottom:0} on this RN/Reanimated/
|
|
168
|
-
// Old-Architecture combo, leaving a blank gap past the real content end).
|
|
169
|
-
const expectedKeyboardInsetRef = useRef(0);
|
|
152
|
+
// Current keyboard height, and the trigger for the re-anchor effect
|
|
153
|
+
// below — see the keyboard listener for how each platform uses it.
|
|
154
|
+
const [keyboardInset, setKeyboardInset] = useState(0);
|
|
155
|
+
const hasSeenFirstKeyboardInsetRef = useRef(false);
|
|
170
156
|
const rowRefsMap = useRef<Map<string, View | null>>(new Map());
|
|
171
157
|
// Set on a real touch-drag (`onScrollBeginDrag`), before onStartReached/
|
|
172
158
|
// onEndReached's threshold could fire. `onMomentumScrollBegin` was tried
|
|
@@ -292,17 +278,14 @@ export const ChatListLegend = memo(
|
|
|
292
278
|
>
|
|
293
279
|
<LegendChatMessage
|
|
294
280
|
message={message}
|
|
281
|
+
messageId={messageId}
|
|
295
282
|
isOutgoing={isOutgoing}
|
|
296
283
|
createdAtTime={createdAt}
|
|
297
284
|
onMediaPress={onMediaPress}
|
|
298
285
|
onPressUrl={onPressUrl}
|
|
299
286
|
onRetry={retryMessage}
|
|
300
|
-
onPress={
|
|
301
|
-
onLongPress={
|
|
302
|
-
onLongPress
|
|
303
|
-
? () => handleMessageLongPress(messageId, message)
|
|
304
|
-
: undefined
|
|
305
|
-
}
|
|
287
|
+
onPress={handleMessagePress}
|
|
288
|
+
onLongPress={onLongPress ? handleMessageLongPress : undefined}
|
|
306
289
|
customMessageItemFactory={customMessageItemFactory}
|
|
307
290
|
isHighlighted={highlightedId === messageId}
|
|
308
291
|
isTimeRevealed={revealedMessageId === messageId}
|
|
@@ -485,47 +468,21 @@ export const ChatListLegend = memo(
|
|
|
485
468
|
);
|
|
486
469
|
|
|
487
470
|
const handleStartReached = useCallback(() => {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
hasMoreOld,
|
|
493
|
-
moreOldLoading: useMessageStore.getState().moreOldLoading,
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
if (!hasUserScrolledRef.current) {
|
|
497
|
-
const wouldHaveLoaded =
|
|
498
|
-
__DEV__ && hasMoreOld && !useMessageStore.getState().moreOldLoading;
|
|
499
|
-
if (wouldHaveLoaded) {
|
|
500
|
-
console.warn(
|
|
501
|
-
`${LOG_TAG} onStartReached fired before user scroll — suppressed`,
|
|
502
|
-
{ conversationId, hasMoreOld }
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
471
|
+
// Both thresholds are checked continuously, including at mount before
|
|
472
|
+
// any gesture — `hasUserScrolledRef` is what keeps that from firing a
|
|
473
|
+
// load-more the user never asked for.
|
|
474
|
+
if (!hasUserScrolledRef.current) return;
|
|
507
475
|
if (hasMoreOld && !useMessageStore.getState().moreOldLoading) {
|
|
508
476
|
onLoadMoreOld?.(true);
|
|
509
477
|
}
|
|
510
|
-
}, [
|
|
478
|
+
}, [hasMoreOld, onLoadMoreOld]);
|
|
511
479
|
|
|
512
480
|
const handleEndReached = useCallback(() => {
|
|
513
|
-
if (!hasUserScrolledRef.current)
|
|
514
|
-
const wouldHaveLoaded =
|
|
515
|
-
__DEV__ && hasMoreNew && !useMessageStore.getState().moreNewLoading;
|
|
516
|
-
if (wouldHaveLoaded) {
|
|
517
|
-
console.warn(
|
|
518
|
-
`${LOG_TAG} onEndReached fired before user scroll — suppressed`,
|
|
519
|
-
{ conversationId, hasMoreNew }
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
481
|
+
if (!hasUserScrolledRef.current) return;
|
|
524
482
|
if (hasMoreNew && !useMessageStore.getState().moreNewLoading) {
|
|
525
|
-
if (__DEV__) console.log(`${LOG_TAG} handleEndReached`);
|
|
526
483
|
onLoadMoreNew?.(true);
|
|
527
484
|
}
|
|
528
|
-
}, [
|
|
485
|
+
}, [hasMoreNew, onLoadMoreNew]);
|
|
529
486
|
|
|
530
487
|
const handleScrollBeginDrag = useCallback(() => {
|
|
531
488
|
hasUserScrolledRef.current = true;
|
|
@@ -533,41 +490,11 @@ export const ChatListLegend = memo(
|
|
|
533
490
|
|
|
534
491
|
const handleScroll = useCallback(
|
|
535
492
|
(e: any) => {
|
|
536
|
-
const { contentOffset, contentSize, layoutMeasurement
|
|
537
|
-
e.nativeEvent;
|
|
493
|
+
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
538
494
|
const isAtBottom =
|
|
539
495
|
contentOffset.y + layoutMeasurement.height >=
|
|
540
496
|
contentSize.height - 100;
|
|
541
497
|
|
|
542
|
-
if (__DEV__) {
|
|
543
|
-
const expected = expectedKeyboardInsetRef.current;
|
|
544
|
-
const actual = contentInset?.bottom ?? 0;
|
|
545
|
-
// `contentInset` failing to apply is a constant, systemic mismatch
|
|
546
|
-
// (present regardless of conversation length) — but it only shows
|
|
547
|
-
// up as a visible gap once the scroll position has actually moved
|
|
548
|
-
// past the real end of content. Gate on that so the warning fires
|
|
549
|
-
// only when the bug is actually visible, not on every keyboard
|
|
550
|
-
// open in every conversation.
|
|
551
|
-
const scrolledPastRealContentEnd =
|
|
552
|
-
contentOffset.y + layoutMeasurement.height > contentSize.height;
|
|
553
|
-
if (
|
|
554
|
-
expected > 0 &&
|
|
555
|
-
Math.abs(actual - expected) > 5 &&
|
|
556
|
-
scrolledPastRealContentEnd
|
|
557
|
-
) {
|
|
558
|
-
console.warn(
|
|
559
|
-
`${LOG_TAG} keyboard contentInset mismatch — expected bottom inset ~${expected} (keyboard open) but native reports ${actual}, and scroll position is past the real content end. This reproduces the known "blank gap" issue: contentOffset scrolls correctly but contentInset silently fails to apply.`,
|
|
560
|
-
{
|
|
561
|
-
expected,
|
|
562
|
-
actual,
|
|
563
|
-
contentOffsetY: contentOffset.y,
|
|
564
|
-
contentSizeHeight: contentSize.height,
|
|
565
|
-
layoutHeight: layoutMeasurement.height,
|
|
566
|
-
}
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
498
|
// Debounce animation updates
|
|
572
499
|
if (scrollDebounceRef.current) {
|
|
573
500
|
clearTimeout(scrollDebounceRef.current);
|
|
@@ -619,26 +546,72 @@ export const ChatListLegend = memo(
|
|
|
619
546
|
};
|
|
620
547
|
}, []);
|
|
621
548
|
|
|
622
|
-
//
|
|
623
|
-
// `
|
|
624
|
-
//
|
|
625
|
-
//
|
|
549
|
+
// Tracks the keyboard's height as the trigger for the re-anchor below.
|
|
550
|
+
// The `willHide` side is debounced because Android's event stream flaps
|
|
551
|
+
// willShow→willHide→willShow roughly every 1.3s for as long as the
|
|
552
|
+
// keyboard stays open (measured on device) — without this, every flap
|
|
553
|
+
// would fire another scroll. A real close still lands, just one tick
|
|
554
|
+
// later. Setting the same height twice is a no-op for the effect below,
|
|
555
|
+
// so a stable open only ever re-anchors once.
|
|
556
|
+
//
|
|
557
|
+
// JS-thread only (no worklet), so none of the reentrant-lock crash risk
|
|
558
|
+
// that reaching into `useKeyboardHandler`'s worklets carries.
|
|
626
559
|
useEffect(() => {
|
|
627
|
-
|
|
560
|
+
let pendingHide: ReturnType<typeof setTimeout> | undefined;
|
|
561
|
+
const clearPendingHide = () => {
|
|
562
|
+
if (pendingHide) {
|
|
563
|
+
clearTimeout(pendingHide);
|
|
564
|
+
pendingHide = undefined;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const applyInset = (nextInset: number) => {
|
|
568
|
+
// Android only: the bottom inset the library applies there is a
|
|
569
|
+
// synthetic prop that `onScroll` does not report (its own
|
|
570
|
+
// `ScrollViewWithBottomPadding` says as much), so LegendList never
|
|
571
|
+
// learns about it, `alignItemsAtEndPadding` never shrinks, and the
|
|
572
|
+
// re-anchor below has no room to scroll into. Reporting it closes
|
|
573
|
+
// that gap. iOS needs none of this — there the inset is real and
|
|
574
|
+
// arrives through `onScroll` on its own, and overriding it here
|
|
575
|
+
// would double-count.
|
|
576
|
+
if (Platform.OS === 'android') {
|
|
577
|
+
listRef.current?.reportContentInset(
|
|
578
|
+
nextInset > 0 ? { bottom: nextInset } : null
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
setKeyboardInset(nextInset);
|
|
582
|
+
};
|
|
583
|
+
|
|
628
584
|
const willShow = KeyboardEvents.addListener('keyboardWillShow', (e) => {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
0
|
|
632
|
-
);
|
|
585
|
+
clearPendingHide();
|
|
586
|
+
applyInset(Math.max(e.height - keyboardOffset, 0));
|
|
633
587
|
});
|
|
634
588
|
const willHide = KeyboardEvents.addListener('keyboardWillHide', () => {
|
|
635
|
-
|
|
589
|
+
clearPendingHide();
|
|
590
|
+
pendingHide = setTimeout(
|
|
591
|
+
() => applyInset(0),
|
|
592
|
+
KEYBOARD_HIDE_DEBOUNCE_MS
|
|
593
|
+
);
|
|
636
594
|
});
|
|
637
595
|
return () => {
|
|
596
|
+
clearPendingHide();
|
|
638
597
|
willShow.remove();
|
|
639
598
|
willHide.remove();
|
|
640
599
|
};
|
|
641
|
-
}, [keyboardOffset]);
|
|
600
|
+
}, [keyboardOffset, listRef]);
|
|
601
|
+
|
|
602
|
+
// Re-anchors to the end once the keyboard's inset has landed, which puts
|
|
603
|
+
// the last message at exactly the spacing the closed-keyboard layout has
|
|
604
|
+
// (verified on device against both a 7-message and a long conversation).
|
|
605
|
+
// Skipped when the user has scrolled away from the bottom — opening the
|
|
606
|
+
// keyboard shouldn't yank them back down.
|
|
607
|
+
useEffect(() => {
|
|
608
|
+
if (!hasSeenFirstKeyboardInsetRef.current) {
|
|
609
|
+
hasSeenFirstKeyboardInsetRef.current = true;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (!isAtBottomRef.current) return;
|
|
613
|
+
scrollMessageToEnd({ animated: true, closeKeyboard: false });
|
|
614
|
+
}, [keyboardInset, scrollMessageToEnd]);
|
|
642
615
|
|
|
643
616
|
// This component can stay mounted across a conversationId change (e.g.
|
|
644
617
|
// navigation.replace reusing the same screen instance), so this ref must
|
|
@@ -668,54 +641,17 @@ export const ChatListLegend = memo(
|
|
|
668
641
|
!moreOldLoadingRef.current &&
|
|
669
642
|
isAtBottomRef.current;
|
|
670
643
|
|
|
671
|
-
if (__DEV__) {
|
|
672
|
-
console.log(
|
|
673
|
-
`${LOG_TAG} lastMessageId effect — ${shouldFire ? 'firing animated scroll' : 'skipped'}`,
|
|
674
|
-
{
|
|
675
|
-
conversationId,
|
|
676
|
-
lastMessageId,
|
|
677
|
-
initialPopulation: previousLastMessageId === null,
|
|
678
|
-
moreOldLoading: moreOldLoadingRef.current,
|
|
679
|
-
isAtBottom: isAtBottomRef.current,
|
|
680
|
-
}
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
644
|
if (!shouldFire) return;
|
|
685
645
|
|
|
686
646
|
scrollMessageToEnd({ animated: true, closeKeyboard: false });
|
|
687
|
-
}, [lastMessageId, scrollMessageToEnd
|
|
647
|
+
}, [lastMessageId, scrollMessageToEnd]);
|
|
688
648
|
|
|
689
|
-
const handleListLoad = useCallback(
|
|
690
|
-
(
|
|
691
|
-
|
|
692
|
-
hasCorrectedInitialPaddingRef.current = true;
|
|
649
|
+
const handleListLoad = useCallback(() => {
|
|
650
|
+
if (hasCorrectedInitialPaddingRef.current) return;
|
|
651
|
+
hasCorrectedInitialPaddingRef.current = true;
|
|
693
652
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
console.log(`${LOG_TAG} onLoad (bootstrap settle done)`, {
|
|
697
|
-
conversationId,
|
|
698
|
-
messagesLength: messages.length,
|
|
699
|
-
elapsedTimeInMs: info.elapsedTimeInMs,
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const startedAt = __DEV__ ? Date.now() : 0;
|
|
704
|
-
const flushPromise = scrollMessageToEnd({
|
|
705
|
-
animated: false,
|
|
706
|
-
closeKeyboard: false,
|
|
707
|
-
});
|
|
708
|
-
if (__DEV__) {
|
|
709
|
-
flushPromise.then(() => {
|
|
710
|
-
console.log(`${LOG_TAG} handleListLoad flush resolved`, {
|
|
711
|
-
conversationId,
|
|
712
|
-
tookMs: Date.now() - startedAt,
|
|
713
|
-
});
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
},
|
|
717
|
-
[scrollMessageToEnd, conversationId, messages.length]
|
|
718
|
-
);
|
|
653
|
+
scrollMessageToEnd({ animated: false, closeKeyboard: false });
|
|
654
|
+
}, [scrollMessageToEnd]);
|
|
719
655
|
|
|
720
656
|
useEffect(() => {
|
|
721
657
|
const bottomSubscription = DeviceEventEmitter.addListener(
|
|
@@ -801,7 +737,7 @@ export const ChatListLegend = memo(
|
|
|
801
737
|
keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
|
|
802
738
|
recycleItems
|
|
803
739
|
extraData={listExtraData}
|
|
804
|
-
alignItemsAtEnd
|
|
740
|
+
alignItemsAtEnd
|
|
805
741
|
alwaysRender={{
|
|
806
742
|
top: 10,
|
|
807
743
|
}}
|
|
@@ -813,15 +749,20 @@ export const ChatListLegend = memo(
|
|
|
813
749
|
drawDistance={800}
|
|
814
750
|
freeze={freeze}
|
|
815
751
|
keyboardOffset={keyboardOffset}
|
|
752
|
+
// The list's own keyboard compensation is off on both platforms —
|
|
753
|
+
// measured, it overshoots badly either way. On iOS it scrolls on
|
|
754
|
+
// top of React Native's `automaticallyAdjustContentInsets` (which
|
|
755
|
+
// already insets the scroll view by the full keyboard height), and
|
|
756
|
+
// that double lift is the blank gap. On Android it overshot by
|
|
757
|
+
// ~690px against a 128px target. What both platforms do keep from
|
|
758
|
+
// the library is the bottom inset it sets, which is the room the
|
|
759
|
+
// re-anchor above scrolls into.
|
|
760
|
+
keyboardLiftBehavior="never"
|
|
816
761
|
scrollEventThrottle={16}
|
|
817
762
|
onScroll={handleScroll}
|
|
818
763
|
onScrollBeginDrag={handleScrollBeginDrag}
|
|
819
764
|
onLoad={handleListLoad}
|
|
820
|
-
contentContainerStyle={
|
|
821
|
-
isVeryShortConversation
|
|
822
|
-
? [styles.content, styles.contentBottomAnchored]
|
|
823
|
-
: styles.content
|
|
824
|
-
}
|
|
765
|
+
contentContainerStyle={styles.content}
|
|
825
766
|
scrollIndicatorInsets={{ right: 1 }}
|
|
826
767
|
ListHeaderComponent={
|
|
827
768
|
<LegendChatLoadEarlier isLoading={moreOldLoading} />
|
|
@@ -872,9 +813,6 @@ const styles = StyleSheet.create({
|
|
|
872
813
|
paddingVertical: KSpacingValue['0.75rem'],
|
|
873
814
|
flexGrow: 1,
|
|
874
815
|
},
|
|
875
|
-
contentBottomAnchored: {
|
|
876
|
-
justifyContent: 'flex-end',
|
|
877
|
-
},
|
|
878
816
|
scrollToBottomButton: {
|
|
879
817
|
position: 'absolute',
|
|
880
818
|
bottom: 20,
|
|
@@ -52,7 +52,11 @@ const PinnedMessagesSheetContent = memo(
|
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
const handlePressMessage = useCallback(
|
|
55
|
-
(
|
|
55
|
+
(messageId: string) => {
|
|
56
|
+
const message = usePinnedMessageStore
|
|
57
|
+
.getState()
|
|
58
|
+
.messages.find((m) => (m.clientMsgID || m.serverMsgID) === messageId);
|
|
59
|
+
if (!message) return;
|
|
56
60
|
UIUtils.bottomSheet.dismiss();
|
|
57
61
|
onPressMessage?.(message);
|
|
58
62
|
},
|
|
@@ -117,6 +121,7 @@ const PinnedMessagesSheetContent = memo(
|
|
|
117
121
|
|
|
118
122
|
<LegendChatMessage
|
|
119
123
|
message={message}
|
|
124
|
+
messageId={messageId}
|
|
120
125
|
isOutgoing={isOutgoing}
|
|
121
126
|
createdAtTime={createdAtTime}
|
|
122
127
|
onPress={handlePressMessage}
|
|
@@ -90,6 +90,7 @@ const PinnedMessageToolboxOverlay = memo(
|
|
|
90
90
|
message={
|
|
91
91
|
<LegendChatMessage
|
|
92
92
|
message={message}
|
|
93
|
+
messageId={message.clientMsgID || message.serverMsgID || ''}
|
|
93
94
|
isOutgoing={isOutgoing}
|
|
94
95
|
createdAtTime={message.sendTime || message.createTime}
|
|
95
96
|
customMessageItemFactory={customMessageItemFactory}
|
|
@@ -191,9 +192,10 @@ const PinnedMessagesSheetContent = memo(
|
|
|
191
192
|
>
|
|
192
193
|
<LegendChatMessage
|
|
193
194
|
message={message}
|
|
195
|
+
messageId={messageId}
|
|
194
196
|
isOutgoing={isOutgoing}
|
|
195
197
|
createdAtTime={createdAtTime}
|
|
196
|
-
onLongPress={
|
|
198
|
+
onLongPress={handleLongPress}
|
|
197
199
|
customMessageItemFactory={customMessageItemFactory}
|
|
198
200
|
/>
|
|
199
201
|
{!!timeLabel && (
|
|
@@ -38,13 +38,15 @@ type MessageProps = BaseLegendMessageProps & {
|
|
|
38
38
|
|
|
39
39
|
interface LegendChatMessageProps {
|
|
40
40
|
message: MessageItem;
|
|
41
|
+
/** Stable per-row id (see getMessageRenderId) — bound into onPress/onLongPress locally so those callbacks stay referentially stable across unrelated list re-renders. */
|
|
42
|
+
messageId: string;
|
|
41
43
|
isOutgoing: boolean;
|
|
42
44
|
createdAtTime: number;
|
|
43
45
|
onMediaPress?: (items: MediaItem[], index?: number) => void;
|
|
44
46
|
onPressUrl?: (url: string) => void;
|
|
45
47
|
onRetry?: (message: MessageItem) => void;
|
|
46
|
-
onPress?: (
|
|
47
|
-
onLongPress?: () => void;
|
|
48
|
+
onPress?: (messageId: string) => void;
|
|
49
|
+
onLongPress?: (messageId: string, message: MessageItem) => void;
|
|
48
50
|
customMessageItemFactory?: (m: MessageItem) => ReactNode;
|
|
49
51
|
isHighlighted?: boolean;
|
|
50
52
|
isTimeRevealed?: boolean;
|
|
@@ -99,6 +101,7 @@ const TIME_REVEAL_EXCLUDED_TYPES: number[] = [
|
|
|
99
101
|
export const LegendChatMessage = memo(
|
|
100
102
|
({
|
|
101
103
|
message,
|
|
104
|
+
messageId,
|
|
102
105
|
isOutgoing,
|
|
103
106
|
createdAtTime,
|
|
104
107
|
onMediaPress,
|
|
@@ -115,12 +118,12 @@ export const LegendChatMessage = memo(
|
|
|
115
118
|
groupPosition,
|
|
116
119
|
}: LegendChatMessageProps) => {
|
|
117
120
|
const handlePress = useCallback(() => {
|
|
118
|
-
onPress?.(
|
|
119
|
-
}, [onPress,
|
|
121
|
+
onPress?.(messageId);
|
|
122
|
+
}, [onPress, messageId]);
|
|
120
123
|
|
|
121
124
|
const handleLongPress = useCallback(() => {
|
|
122
|
-
onLongPress?.();
|
|
123
|
-
}, [onLongPress]);
|
|
125
|
+
onLongPress?.(messageId, message);
|
|
126
|
+
}, [onLongPress, messageId, message]);
|
|
124
127
|
|
|
125
128
|
const customNode = customMessageItemFactory?.(message);
|
|
126
129
|
|
|
@@ -22,7 +22,7 @@ import { MessageToolboxWrapper } from './MessageToolboxWrapper';
|
|
|
22
22
|
import type { ReactNode } from 'react';
|
|
23
23
|
import type { SharedValue } from 'react-native-reanimated';
|
|
24
24
|
import { useSendMessage } from '../../../hooks/message/useSendMessage';
|
|
25
|
-
import { useChatDetailContext } from '../../../context';
|
|
25
|
+
import { useChatContext, useChatDetailContext } from '../../../context';
|
|
26
26
|
import {
|
|
27
27
|
GroupPermission,
|
|
28
28
|
type MessageItem,
|
|
@@ -64,7 +64,8 @@ export const MessageToolboxOverlay = forwardRef<
|
|
|
64
64
|
},
|
|
65
65
|
ref
|
|
66
66
|
) => {
|
|
67
|
-
const { setQuotedMessage } = useChatDetailContext();
|
|
67
|
+
const { setQuotedMessage, conversation } = useChatDetailContext();
|
|
68
|
+
const { generateChatDetailLink } = useChatContext();
|
|
68
69
|
const [visible, setVisible] = useState(false);
|
|
69
70
|
const [message, setMessage] = useState<MessageItem | undefined>(undefined);
|
|
70
71
|
const [rect, setRect] = useState<MeasuredRect | null>(null);
|
|
@@ -139,16 +140,24 @@ export const MessageToolboxOverlay = forwardRef<
|
|
|
139
140
|
}, [message, hide]);
|
|
140
141
|
|
|
141
142
|
const handleCopyMessageLink = useCallback(() => {
|
|
142
|
-
if (!message) return;
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
143
|
+
if (!message || !generateChatDetailLink) return;
|
|
144
|
+
|
|
145
|
+
const link = generateChatDetailLink({
|
|
146
|
+
targetConversationId: conversation?.conversationID,
|
|
147
|
+
targetMessageId: message.clientMsgID,
|
|
148
|
+
targetGroupId: conversation?.groupID,
|
|
149
|
+
targetUserId: conversation?.userID,
|
|
150
|
+
// targetBotId: '',
|
|
151
|
+
});
|
|
152
|
+
Clipboard.setString(link);
|
|
153
|
+
|
|
146
154
|
UIUtils.toast.open({
|
|
147
155
|
title: trans('chat:copy_message_link_success'),
|
|
148
156
|
theme: 'success',
|
|
149
157
|
});
|
|
158
|
+
|
|
150
159
|
hide();
|
|
151
|
-
}, [message, hide]);
|
|
160
|
+
}, [message, hide, generateChatDetailLink, conversation]);
|
|
152
161
|
|
|
153
162
|
const handleRetryMessage = useCallback(() => {
|
|
154
163
|
if (!message) return;
|
|
@@ -214,6 +223,7 @@ export const MessageToolboxOverlay = forwardRef<
|
|
|
214
223
|
message={
|
|
215
224
|
<LegendChatMessage
|
|
216
225
|
message={message}
|
|
226
|
+
messageId={message.clientMsgID || message.serverMsgID || ''}
|
|
217
227
|
isOutgoing={isOutgoing}
|
|
218
228
|
createdAtTime={message.sendTime || message.createTime}
|
|
219
229
|
customMessageItemFactory={customMessageItemFactory}
|
|
@@ -222,6 +232,9 @@ export const MessageToolboxOverlay = forwardRef<
|
|
|
222
232
|
toolbox={
|
|
223
233
|
<MessageToolboxMenu
|
|
224
234
|
{...toolboxItems}
|
|
235
|
+
showCopyMessageLink={
|
|
236
|
+
toolboxItems.showCopyMessageLink && !!generateChatDetailLink
|
|
237
|
+
}
|
|
225
238
|
showPinMessage={toolboxItems.showPinMessage && isPinAllowed}
|
|
226
239
|
showUnpinMessage={toolboxItems.showUnpinMessage && isPinAllowed}
|
|
227
240
|
onCopyMessage={handleCopyMessage}
|
|
@@ -38,8 +38,8 @@ const isCopyMessageSupported = (message: MessageItem): boolean =>
|
|
|
38
38
|
COPYABLE_TEXT_TYPES.includes(message.contentType);
|
|
39
39
|
|
|
40
40
|
// All message types are supported, as long as the message is eligible.
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const isCopyMessageLinkSupported = (message: MessageItem): boolean =>
|
|
42
|
+
isEligibleForToolbox(message);
|
|
43
43
|
|
|
44
44
|
const isRetrySupported = (message: MessageItem): boolean =>
|
|
45
45
|
message.status === MessageStatus.Failed;
|
|
@@ -92,7 +92,7 @@ export const getMessageToolboxItems = (
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const showCopyMessage = isCopyMessageSupported(message);
|
|
95
|
-
|
|
95
|
+
const showCopyMessageLink = isCopyMessageLinkSupported(message);
|
|
96
96
|
const showRetryMessage = isRetrySupported(message);
|
|
97
97
|
const showRevokeMessage = isRevokeMessageSupported(message, currentUserId);
|
|
98
98
|
const showQuoteMessage = isQuoteMessageSupported(message);
|
|
@@ -103,7 +103,7 @@ export const getMessageToolboxItems = (
|
|
|
103
103
|
|
|
104
104
|
return {
|
|
105
105
|
showCopyMessage,
|
|
106
|
-
|
|
106
|
+
showCopyMessageLink,
|
|
107
107
|
showRetryMessage,
|
|
108
108
|
showRevokeMessage,
|
|
109
109
|
showPinMessage,
|
package/src/services/apis.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AxiosInstance } from 'axios';
|
|
2
2
|
import OpenIMSDK, { LoginStatus } from '@droppii/openim-rn-client-sdk';
|
|
3
|
-
import { useConversationStore } from '../store';
|
|
3
|
+
import { useConversationStore, useUserStore } from '../store';
|
|
4
4
|
|
|
5
5
|
export let apiInstance: AxiosInstance | null = null;
|
|
6
6
|
|
|
@@ -15,6 +15,7 @@ export namespace ChatAPI {
|
|
|
15
15
|
|
|
16
16
|
export const logout = async (operationID?: string) => {
|
|
17
17
|
useConversationStore.getState().reset();
|
|
18
|
+
useUserStore.getState().reset();
|
|
18
19
|
return OpenIMSDK.logout(operationID);
|
|
19
20
|
};
|
|
20
21
|
}
|
package/src/store/message.ts
CHANGED
|
@@ -191,7 +191,7 @@ export const useMessageStore = create<MessageStore>()((set, get) => ({
|
|
|
191
191
|
hasMoreNew: !searchNextRes.isEnd,
|
|
192
192
|
initLoading: false,
|
|
193
193
|
});
|
|
194
|
-
if (
|
|
194
|
+
if (newMessages?.length > 0) {
|
|
195
195
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
196
196
|
DeviceEventEmitter.emit(ChatEvent.ScrollToMessage, {
|
|
197
197
|
clientMessageId: startClientMsgId,
|
package/src/store/user.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface UserStore {
|
|
|
11
11
|
updateConnectStatus: (status: ConnectStatus) => void;
|
|
12
12
|
syncStatus: SyncStatus;
|
|
13
13
|
updateSyncStatus: (status: SyncStatus) => void;
|
|
14
|
+
reset: () => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const useUserStore = create<UserStore>()((set) => ({
|
|
@@ -28,4 +29,10 @@ export const useUserStore = create<UserStore>()((set) => ({
|
|
|
28
29
|
updateSyncStatus(syncStatus) {
|
|
29
30
|
set({ syncStatus });
|
|
30
31
|
},
|
|
32
|
+
reset() {
|
|
33
|
+
set({
|
|
34
|
+
user: undefined,
|
|
35
|
+
token: undefined,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
31
38
|
}));
|
|
@@ -35,6 +35,8 @@ const i18nKey: Record<string, string> = {
|
|
|
35
35
|
'chat:copy_message_link': 'Sao chép đường dẫn tin nhắn',
|
|
36
36
|
'chat:copy_message_success': 'Sao chép tin nhắn thành công',
|
|
37
37
|
'chat:copy_message_link_success': 'Sao chép đường dẫn tin nhắn thành công',
|
|
38
|
+
'chat:target_message_unavailable':
|
|
39
|
+
'Tin nhắn có thể đã bị xoá và không còn tồn tại',
|
|
38
40
|
'chat:retry_message': 'Gửi lại',
|
|
39
41
|
'chat:revoke_message': 'Thu hồi',
|
|
40
42
|
'chat:pin_message': 'Ghim',
|
package/src/types/chat.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
IRunConversationFlowPayload,
|
|
16
16
|
} from './flows';
|
|
17
17
|
import type { GetOpenIMTokenResponse } from './auth';
|
|
18
|
+
import type { ChatDetailProps } from '../screens/chat-detail';
|
|
18
19
|
|
|
19
20
|
export const EventProvider = {
|
|
20
21
|
ga: 'ga',
|
|
@@ -35,6 +36,17 @@ export interface ChatUIAdapter {
|
|
|
35
36
|
bottomSheet?: { open: (params: any) => void; dismiss: () => void };
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
export type GenerateChatDetailLinkFn = (
|
|
40
|
+
params: Pick<
|
|
41
|
+
ChatDetailProps,
|
|
42
|
+
| 'targetConversationId'
|
|
43
|
+
| 'targetUserId'
|
|
44
|
+
| 'targetGroupId'
|
|
45
|
+
| 'targetBotId'
|
|
46
|
+
| 'targetMessageId'
|
|
47
|
+
>
|
|
48
|
+
) => string;
|
|
49
|
+
|
|
38
50
|
export interface ChatContextType {
|
|
39
51
|
logGA?: LogGA;
|
|
40
52
|
applicationType: DChatApplicationType;
|
|
@@ -42,6 +54,7 @@ export interface ChatContextType {
|
|
|
42
54
|
isShowFLashMessage: boolean;
|
|
43
55
|
setIsShowFLashMessage: (value: boolean) => void;
|
|
44
56
|
handleLoginSdk: () => void;
|
|
57
|
+
generateChatDetailLink?: GenerateChatDetailLinkFn;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
export type ChatProviderProps = PropsWithChildren<{
|
|
@@ -49,6 +62,7 @@ export type ChatProviderProps = PropsWithChildren<{
|
|
|
49
62
|
applicationType: DChatApplicationType;
|
|
50
63
|
getChatToken: () => Promise<GetOpenIMTokenResponse | null>;
|
|
51
64
|
onFlashMessagePress?: (c: ConversationItem) => void;
|
|
65
|
+
generateChatDetailLink?: GenerateChatDetailLinkFn;
|
|
52
66
|
enabled?: boolean;
|
|
53
67
|
}>;
|
|
54
68
|
|