@droppii-org/chat-mobile 0.2.108 → 0.2.110

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 (114) hide show
  1. package/lib/module/screens/ChatInfo/ChatInfo.js +12 -3
  2. package/lib/module/screens/ChatInfo/ChatInfo.js.map +1 -1
  3. package/lib/module/screens/ChatInfo/ChatInfoActions.js +45 -40
  4. package/lib/module/screens/ChatInfo/ChatInfoActions.js.map +1 -1
  5. package/lib/module/screens/ChatInfo/ChatInfoProfile.js +11 -7
  6. package/lib/module/screens/ChatInfo/ChatInfoProfile.js.map +1 -1
  7. package/lib/module/screens/chat-detail/ChatDetail.js +42 -7
  8. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  9. package/lib/module/screens/chat-detail/ChatListLegend.js +249 -49
  10. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  11. package/lib/module/screens/chat-detail/hooks/createUnreadMarkReadGate.js +32 -0
  12. package/lib/module/screens/chat-detail/hooks/createUnreadMarkReadGate.js.map +1 -0
  13. package/lib/module/screens/chat-detail/hooks/useMarkConversationRead.js +180 -0
  14. package/lib/module/screens/chat-detail/hooks/useMarkConversationRead.js.map +1 -0
  15. package/lib/module/screens/chat-detail/hooks/useUnreadAnchorSeen.js +88 -0
  16. package/lib/module/screens/chat-detail/hooks/useUnreadAnchorSeen.js.map +1 -0
  17. package/lib/module/screens/chat-detail/hooks/useUnreadDeeplinkCheck.js +59 -0
  18. package/lib/module/screens/chat-detail/hooks/useUnreadDeeplinkCheck.js.map +1 -0
  19. package/lib/module/screens/chat-detail/hooks/useUnreadIndicator.js +105 -0
  20. package/lib/module/screens/chat-detail/hooks/useUnreadIndicator.js.map +1 -0
  21. package/lib/module/screens/chat-detail/hooks/useUnreadIndicatorInList.js +157 -0
  22. package/lib/module/screens/chat-detail/hooks/useUnreadIndicatorInList.js.map +1 -0
  23. package/lib/module/screens/chat-detail/legend/LegendChatUnreadButton.js +140 -0
  24. package/lib/module/screens/chat-detail/legend/LegendChatUnreadButton.js.map +1 -0
  25. package/lib/module/screens/chat-detail/legend/LegendChatUnreadDivider.js +51 -0
  26. package/lib/module/screens/chat-detail/legend/LegendChatUnreadDivider.js.map +1 -0
  27. package/lib/module/services/message.js +30 -0
  28. package/lib/module/services/message.js.map +1 -1
  29. package/lib/module/store/message.js +25 -4
  30. package/lib/module/store/message.js.map +1 -1
  31. package/lib/module/store/unreadIndicator.js +26 -0
  32. package/lib/module/store/unreadIndicator.js.map +1 -0
  33. package/lib/module/translation/resources/i18n.js +2 -0
  34. package/lib/module/translation/resources/i18n.js.map +1 -1
  35. package/lib/module/types/chat.js.map +1 -1
  36. package/lib/module/types/unreadIndicator.js +2 -0
  37. package/lib/module/types/unreadIndicator.js.map +1 -0
  38. package/lib/module/utils/markReadPolicy.js +25 -0
  39. package/lib/module/utils/markReadPolicy.js.map +1 -0
  40. package/lib/module/utils/promise.js +16 -0
  41. package/lib/module/utils/promise.js.map +1 -1
  42. package/lib/module/utils/unreadAnchor.js +57 -0
  43. package/lib/module/utils/unreadAnchor.js.map +1 -0
  44. package/lib/module/utils/unreadIndicatorState.js +96 -0
  45. package/lib/module/utils/unreadIndicatorState.js.map +1 -0
  46. package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts.map +1 -1
  47. package/lib/typescript/src/screens/ChatInfo/ChatInfoActions.d.ts +2 -1
  48. package/lib/typescript/src/screens/ChatInfo/ChatInfoActions.d.ts.map +1 -1
  49. package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts +2 -1
  50. package/lib/typescript/src/screens/ChatInfo/ChatInfoProfile.d.ts.map +1 -1
  51. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  52. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +4 -1
  53. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  54. package/lib/typescript/src/screens/chat-detail/hooks/createUnreadMarkReadGate.d.ts +9 -0
  55. package/lib/typescript/src/screens/chat-detail/hooks/createUnreadMarkReadGate.d.ts.map +1 -0
  56. package/lib/typescript/src/screens/chat-detail/hooks/useMarkConversationRead.d.ts +39 -0
  57. package/lib/typescript/src/screens/chat-detail/hooks/useMarkConversationRead.d.ts.map +1 -0
  58. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadAnchorSeen.d.ts +26 -0
  59. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadAnchorSeen.d.ts.map +1 -0
  60. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadDeeplinkCheck.d.ts +17 -0
  61. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadDeeplinkCheck.d.ts.map +1 -0
  62. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadIndicator.d.ts +21 -0
  63. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadIndicator.d.ts.map +1 -0
  64. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadIndicatorInList.d.ts +35 -0
  65. package/lib/typescript/src/screens/chat-detail/hooks/useUnreadIndicatorInList.d.ts.map +1 -0
  66. package/lib/typescript/src/screens/chat-detail/legend/LegendChatUnreadButton.d.ts +8 -0
  67. package/lib/typescript/src/screens/chat-detail/legend/LegendChatUnreadButton.d.ts.map +1 -0
  68. package/lib/typescript/src/screens/chat-detail/legend/LegendChatUnreadDivider.d.ts +7 -0
  69. package/lib/typescript/src/screens/chat-detail/legend/LegendChatUnreadDivider.d.ts.map +1 -0
  70. package/lib/typescript/src/services/message.d.ts +8 -0
  71. package/lib/typescript/src/services/message.d.ts.map +1 -1
  72. package/lib/typescript/src/store/message.d.ts.map +1 -1
  73. package/lib/typescript/src/store/unreadIndicator.d.ts +10 -0
  74. package/lib/typescript/src/store/unreadIndicator.d.ts.map +1 -0
  75. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  76. package/lib/typescript/src/types/chat.d.ts +3 -0
  77. package/lib/typescript/src/types/chat.d.ts.map +1 -1
  78. package/lib/typescript/src/types/events.d.ts +17 -1
  79. package/lib/typescript/src/types/events.d.ts.map +1 -1
  80. package/lib/typescript/src/types/unreadIndicator.d.ts +45 -0
  81. package/lib/typescript/src/types/unreadIndicator.d.ts.map +1 -0
  82. package/lib/typescript/src/utils/markReadPolicy.d.ts +27 -0
  83. package/lib/typescript/src/utils/markReadPolicy.d.ts.map +1 -0
  84. package/lib/typescript/src/utils/promise.d.ts +6 -0
  85. package/lib/typescript/src/utils/promise.d.ts.map +1 -1
  86. package/lib/typescript/src/utils/unreadAnchor.d.ts +33 -0
  87. package/lib/typescript/src/utils/unreadAnchor.d.ts.map +1 -0
  88. package/lib/typescript/src/utils/unreadIndicatorState.d.ts +26 -0
  89. package/lib/typescript/src/utils/unreadIndicatorState.d.ts.map +1 -0
  90. package/package.json +2 -2
  91. package/src/screens/ChatInfo/ChatInfo.tsx +10 -1
  92. package/src/screens/ChatInfo/ChatInfoActions.tsx +47 -39
  93. package/src/screens/ChatInfo/ChatInfoProfile.tsx +17 -12
  94. package/src/screens/chat-detail/ChatDetail.tsx +58 -5
  95. package/src/screens/chat-detail/ChatListLegend.tsx +314 -46
  96. package/src/screens/chat-detail/hooks/createUnreadMarkReadGate.ts +41 -0
  97. package/src/screens/chat-detail/hooks/useMarkConversationRead.ts +197 -0
  98. package/src/screens/chat-detail/hooks/useUnreadAnchorSeen.ts +105 -0
  99. package/src/screens/chat-detail/hooks/useUnreadDeeplinkCheck.ts +68 -0
  100. package/src/screens/chat-detail/hooks/useUnreadIndicator.ts +112 -0
  101. package/src/screens/chat-detail/hooks/useUnreadIndicatorInList.ts +181 -0
  102. package/src/screens/chat-detail/legend/LegendChatUnreadButton.tsx +165 -0
  103. package/src/screens/chat-detail/legend/LegendChatUnreadDivider.tsx +52 -0
  104. package/src/services/message.ts +42 -0
  105. package/src/store/message.ts +23 -5
  106. package/src/store/unreadIndicator.ts +38 -0
  107. package/src/translation/resources/i18n.ts +2 -0
  108. package/src/types/chat.ts +3 -0
  109. package/src/types/events.ts +18 -1
  110. package/src/types/unreadIndicator.ts +49 -0
  111. package/src/utils/markReadPolicy.ts +55 -0
  112. package/src/utils/promise.ts +20 -0
  113. package/src/utils/unreadAnchor.ts +87 -0
  114. package/src/utils/unreadIndicatorState.ts +131 -0
@@ -10,4 +10,10 @@ export type SettledResult<T> = {
10
10
  * ship it, so a rejection would otherwise short-circuit Promise.all.
11
11
  */
12
12
  export declare const allSettled: <T extends readonly unknown[]>(promises: readonly [...T]) => Promise<{ [K in keyof T]: SettledResult<Awaited<T[K]>>; }>;
13
+ /**
14
+ * Rejects with `Error('timeout')` if `promise` hasn't settled within `ms`.
15
+ * A settle that lands after the timeout is ignored, and the timer is always
16
+ * cleared on settle so nothing is left pending.
17
+ */
18
+ export declare const withTimeout: <T>(promise: Promise<T>, ms: number) => Promise<T>;
13
19
  //# sourceMappingURL=promise.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../../../src/utils/promise.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,SAAS,OAAO,EAAE,EACrD,UAAU,SAAS,CAAC,GAAG,CAAC,CAAC,KACxB,OAAO,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAE,CAOK,CAAC"}
1
+ {"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../../../src/utils/promise.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,SAAS,OAAO,EAAE,EACrD,UAAU,SAAS,CAAC,GAAG,CAAC,CAAC,KACxB,OAAO,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAE,CAOK,CAAC;AAEjE;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,EAAE,SAAS,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,MAAM,KAAG,OAAO,CAAC,CAAC,CAatE,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { MessageItem } from '@droppii/openim-rn-client-sdk';
2
+ import type { UnreadAnchor } from '../types/unreadIndicator';
3
+ /** Height of the divider at the top of the anchor row (Figma: 27px). */
4
+ export declare const UNREAD_DIVIDER_HEIGHT = 27;
5
+ export declare const messageBelongsToConversation: (message: Pick<MessageItem, "sendID" | "recvID" | "groupID">, conversationId: string) => boolean;
6
+ /**
7
+ * Validates the server's first-unread message before it's trusted: a finite
8
+ * positive `seq`, a non-empty `clientMsgID`, and a message that belongs to
9
+ * this conversation. Anything else → `null` (no indicator).
10
+ */
11
+ export declare const toUnreadAnchor: (message: MessageItem | null | undefined, conversationId: string) => UnreadAnchor | null;
12
+ /**
13
+ * The list row that carries the divider: the first RENDERED message at or
14
+ * after the anchor. The anchor itself may be filtered out of the rendered list
15
+ * (internal / bot notification messages), so the match is by `seq` as well as
16
+ * by id. Only valid when the boundary is inside the loaded window — the raw
17
+ * window's oldest `seq` is at or before the anchor, or there's no older
18
+ * history — otherwise `null` (the anchor is out of window).
19
+ * Returns the row's list key (same as `keyExtractor` for real messages).
20
+ */
21
+ export declare const resolveUnreadAnchorRowId: (rendered: readonly MessageItem[], anchor: UnreadAnchor | null, window: {
22
+ oldestLoadedSeq: number;
23
+ hasMoreOld: boolean;
24
+ }) => string | null;
25
+ /**
26
+ * Whether the top edge of a row (where the divider sits) is inside the
27
+ * viewport with at least `minVisiblePx` of it showing. A tall row clipped at
28
+ * the top — only its bottom part visible — does NOT count.
29
+ */
30
+ export declare const isRowTopInViewport: (rowTop: number | undefined, scroll: number, scrollLength: number, minVisiblePx?: number) => boolean;
31
+ /** D11: a deeplink to a message older than the first unread hides the indicator. */
32
+ export declare const isDeeplinkTargetBeforeAnchor: (targetSeq?: number, anchorSeq?: number) => boolean;
33
+ //# sourceMappingURL=unreadAnchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unreadAnchor.d.ts","sourceRoot":"","sources":["../../../../src/utils/unreadAnchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAO7D,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,eAAO,MAAM,4BAA4B,GACvC,SAAS,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC,EAC3D,gBAAgB,MAAM,KACrB,OAKmE,CAAC;AAEvE;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,SAAS,WAAW,GAAG,IAAI,GAAG,SAAS,EACvC,gBAAgB,MAAM,KACrB,YAAY,GAAG,IASjB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,SAAS,WAAW,EAAE,EAChC,QAAQ,YAAY,GAAG,IAAI,EAC3B,QAAQ;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,KACvD,MAAM,GAAG,IAWX,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,MAAM,EACd,cAAc,MAAM,EACpB,qBAAoC,KACnC,OAG6C,CAAC;AAEjD,oFAAoF;AACpF,eAAO,MAAM,4BAA4B,GACvC,YAAY,MAAM,EAClB,YAAY,MAAM,KACjB,OACmE,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { BeginUnreadSessionOptions, UnreadAnchor, UnreadIndicatorState } from '../types/unreadIndicator';
2
+ /**
3
+ * Pure transitions behind `useUnreadIndicatorStore` — kept free of React and
4
+ * zustand so they can be unit-tested. Every transition after `begin` is
5
+ * token-scoped: a token that isn't the current session's leaves the state
6
+ * untouched (same reference).
7
+ */
8
+ export declare const UNREAD_GATE_WATCHDOG_SLACK_MS = 500;
9
+ export declare const INITIAL_UNREAD_INDICATOR_STATE: UnreadIndicatorState;
10
+ export declare const beginUnreadSession: (s: UnreadIndicatorState, { routeKey, conversationId, skip, awaitDeeplinkCheck, }: BeginUnreadSessionOptions, now: number) => UnreadIndicatorState;
11
+ export declare const resolveUnreadSession: (s: UnreadIndicatorState, token: number, anchor: UnreadAnchor | null) => UnreadIndicatorState;
12
+ export declare const clearUnreadSession: (s: UnreadIndicatorState, token: number) => UnreadIndicatorState;
13
+ export declare const dismissUnreadPill: (s: UnreadIndicatorState, token: number) => UnreadIndicatorState;
14
+ export declare const confirmUnreadDeeplinkCheck: (s: UnreadIndicatorState, token: number, hide: boolean) => UnreadIndicatorState;
15
+ export declare const rearmUnreadDeeplinkCheck: (s: UnreadIndicatorState, token: number) => UnreadIndicatorState;
16
+ export declare const resetUnreadSession: (s: UnreadIndicatorState) => UnreadIndicatorState;
17
+ /** When the watchdog opens a still-pending gate on its own. */
18
+ export declare const getUnreadGateDeadline: (s: UnreadIndicatorState, timeoutMs: number) => number;
19
+ /**
20
+ * Whether mark-read must wait. Closed only for MY session while its lookup is
21
+ * still pending and within the watchdog deadline. No session yet (not focused,
22
+ * or access not confirmed) is fail-closed; a session superseded by another
23
+ * visit is not this screen's gate.
24
+ */
25
+ export declare const isUnreadGateClosed: (s: UnreadIndicatorState, myToken: number | null, now: number, timeoutMs: number) => boolean;
26
+ //# sourceMappingURL=unreadIndicatorState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unreadIndicatorState.d.ts","sourceRoot":"","sources":["../../../../src/utils/unreadIndicatorState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,YAAY,EACZ,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAElC;;;;;GAKG;AAIH,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD,eAAO,MAAM,8BAA8B,EAAE,oBAU5C,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAC7B,GAAG,oBAAoB,EACvB,yDAKG,yBAAyB,EAC5B,KAAK,MAAM,KACV,oBAQD,CAAC;AAEH,eAAO,MAAM,oBAAoB,GAC/B,GAAG,oBAAoB,EACvB,OAAO,MAAM,EACb,QAAQ,YAAY,GAAG,IAAI,KAC1B,oBAGyD,CAAC;AAI7D,eAAO,MAAM,kBAAkB,GAC7B,GAAG,oBAAoB,EACvB,OAAO,MAAM,KACZ,oBAUI,CAAC;AAER,eAAO,MAAM,iBAAiB,GAC5B,GAAG,oBAAoB,EACvB,OAAO,MAAM,KACZ,oBACuE,CAAC;AAE3E,eAAO,MAAM,0BAA0B,GACrC,GAAG,oBAAoB,EACvB,OAAO,MAAM,EACb,MAAM,OAAO,KACZ,oBAMF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,GAAG,oBAAoB,EACvB,OAAO,MAAM,KACZ,oBAGwC,CAAC;AAE5C,eAAO,MAAM,kBAAkB,GAC7B,GAAG,oBAAoB,KACtB,oBAGD,CAAC;AAEH,+DAA+D;AAC/D,eAAO,MAAM,qBAAqB,GAChC,GAAG,oBAAoB,EACvB,WAAW,MAAM,KAChB,MAAoE,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,GAAG,oBAAoB,EACvB,SAAS,MAAM,GAAG,IAAI,EACtB,KAAK,MAAM,EACX,WAAW,MAAM,KAChB,OAIF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droppii-org/chat-mobile",
3
- "version": "0.2.108",
3
+ "version": "0.2.110",
4
4
  "description": "Droppii chat mobile",
5
5
  "keywords": [
6
6
  "react-native",
@@ -70,7 +70,7 @@
70
70
  "example-legacy": "yarn workspace @droppii-org/chat-mobile-example-legacy"
71
71
  },
72
72
  "dependencies": {
73
- "@droppii/openim-rn-client-sdk": "git+ssh://git@github.com/droppii/open-im-sdk-reactnative.git#v1.0.0-rc41",
73
+ "@droppii/openim-rn-client-sdk": "git+ssh://git@github.com/droppii/open-im-sdk-reactnative.git#v1.0.0-rc44",
74
74
  "@legendapp/list": "^3.3.10",
75
75
  "axios": "^1.16.1",
76
76
  "lodash": "^4.18.1",
@@ -1,4 +1,4 @@
1
- import { memo, useCallback } from 'react';
1
+ import { memo, useCallback, useMemo } from 'react';
2
2
  import { KContainer, KNavigation } from '@droppii/libs';
3
3
  import { useNavigation } from '@react-navigation/native';
4
4
  import { useGetConversationTarget } from '../../hooks/conversation/useGetConversationDetail';
@@ -14,6 +14,7 @@ import { trans } from '../../translation';
14
14
  import { withChatDetailProvider } from '../../context';
15
15
  import { useChatDetailContext } from '../../context';
16
16
  import { getHeaderIconColor } from '../../utils/theme';
17
+ import { useIsJoinedGroup } from '../../hooks/useIsJoinedGroup';
17
18
 
18
19
  const ChatInfo = memo(
19
20
  ({
@@ -39,6 +40,12 @@ const ChatInfo = memo(
39
40
  );
40
41
  const { leaveGroup } = useLeaveGroup();
41
42
 
43
+ const { data: isJoinedGroup } = useIsJoinedGroup(conversation?.groupID);
44
+
45
+ const isNotInGroup = useMemo(() => {
46
+ return isGroup ? !isJoinedGroup : false;
47
+ }, [isGroup, isJoinedGroup]);
48
+
42
49
  const handleLeaveGroup = useCallback(async () => {
43
50
  if (!conversation?.groupID) return;
44
51
 
@@ -78,6 +85,7 @@ const ChatInfo = memo(
78
85
  <ChatInfoProfile
79
86
  conversation={conversation}
80
87
  onPressSearch={onPressSearch}
88
+ isNotInGroup={isNotInGroup}
81
89
  />
82
90
  <ChatInfoMediaRow onPressMedia={onPressMedia} />
83
91
  <KContainer.VisibleView visible={isGroup}>
@@ -92,6 +100,7 @@ const ChatInfo = memo(
92
100
  onPressAddToGroup={onPressAddToGroup}
93
101
  onPressLeaveGroup={handleLeaveGroup}
94
102
  onDismissGroup={onDismissGroup}
103
+ isNotInGroup={isNotInGroup}
95
104
  />
96
105
  </KContainer.ScrollView>
97
106
  </KContainer.Page>
@@ -26,6 +26,7 @@ interface ChatInfoActionsProps {
26
26
  onPressAddToGroup?: () => void;
27
27
  onPressLeaveGroup?: () => void;
28
28
  onDismissGroup?: () => void;
29
+ isNotInGroup?: boolean;
29
30
  }
30
31
 
31
32
  const ChatInfoActions = memo(
@@ -38,6 +39,7 @@ const ChatInfoActions = memo(
38
39
  onPressAddToGroup,
39
40
  onPressLeaveGroup,
40
41
  onDismissGroup,
42
+ isNotInGroup,
41
43
  }: ChatInfoActionsProps) => {
42
44
  const { pinConversation, unpinConversation } = usePinConversation();
43
45
  const isPinned = Boolean(conversation?.isPinned);
@@ -113,22 +115,24 @@ const ChatInfoActions = memo(
113
115
  }
114
116
  }
115
117
 
116
- items.push({
117
- leftNode: {
118
- icon: { vectorName: isPinned ? 'thumbtack-off-o' : 'thumbtack-o' },
119
- },
120
- onPress: handlePinPress,
121
- showChevron: false,
122
- paddingH: '0.75rem',
123
- paddingV: '0.75rem',
124
- customLabel: (
125
- <KLabel.Text typo="TextMdMedium" numberOfLines={1}>
126
- {isPinned
127
- ? trans('chat:info_unpin_message')
128
- : trans('chat:info_pin_message')}
129
- </KLabel.Text>
130
- ),
131
- });
118
+ if (!isNotInGroup) {
119
+ items.push({
120
+ leftNode: {
121
+ icon: { vectorName: isPinned ? 'thumbtack-off-o' : 'thumbtack-o' },
122
+ },
123
+ onPress: handlePinPress,
124
+ showChevron: false,
125
+ paddingH: '0.75rem',
126
+ paddingV: '0.75rem',
127
+ customLabel: (
128
+ <KLabel.Text typo="TextMdMedium" numberOfLines={1}>
129
+ {isPinned
130
+ ? trans('chat:info_unpin_message')
131
+ : trans('chat:info_pin_message')}
132
+ </KLabel.Text>
133
+ ),
134
+ });
135
+ }
132
136
 
133
137
  return items;
134
138
  }, [
@@ -141,11 +145,12 @@ const ChatInfoActions = memo(
141
145
  onPressCreateGroup,
142
146
  onPressAddToGroup,
143
147
  handlePinPress,
148
+ isNotInGroup,
144
149
  ]);
145
150
 
146
151
  const dangerData: KListItemBaseItemProps[] = useMemo(() => {
147
152
  const items: KListItemBaseItemProps[] = [];
148
- if (isGroup) {
153
+ if (isGroup && !isNotInGroup) {
149
154
  items.push({
150
155
  leftNode: {
151
156
  icon: {
@@ -194,29 +199,31 @@ const ChatInfoActions = memo(
194
199
  }
195
200
  }
196
201
 
197
- items.push({
198
- label: trans('chat:info_delete_history'),
199
- leftNode: {
200
- icon: {
201
- vectorName: 'trash-alt-o-1',
202
- tintColor: KColors.danger.normal,
202
+ if (!isNotInGroup) {
203
+ items.push({
204
+ label: trans('chat:info_delete_history'),
205
+ leftNode: {
206
+ icon: {
207
+ vectorName: 'trash-alt-o-1',
208
+ tintColor: KColors.danger.normal,
209
+ },
203
210
  },
204
- },
205
- tintColor: KColors.danger.normal,
206
- onPress: handleDeleteHistory,
207
- showChevron: false,
208
- paddingH: '0.75rem',
209
- paddingV: '0.75rem',
210
- customLabel: (
211
- <KLabel.Text
212
- typo="TextMdMedium"
213
- color={KColors.danger.normal}
214
- numberOfLines={1}
215
- >
216
- {trans('chat:info_delete_history')}
217
- </KLabel.Text>
218
- ),
219
- });
211
+ tintColor: KColors.danger.normal,
212
+ onPress: handleDeleteHistory,
213
+ showChevron: false,
214
+ paddingH: '0.75rem',
215
+ paddingV: '0.75rem',
216
+ customLabel: (
217
+ <KLabel.Text
218
+ typo="TextMdMedium"
219
+ color={KColors.danger.normal}
220
+ numberOfLines={1}
221
+ >
222
+ {trans('chat:info_delete_history')}
223
+ </KLabel.Text>
224
+ ),
225
+ });
226
+ }
220
227
 
221
228
  return items;
222
229
  }, [
@@ -225,6 +232,7 @@ const ChatInfoActions = memo(
225
232
  handleDeleteHistory,
226
233
  isOwner,
227
234
  handleDismissGroup,
235
+ isNotInGroup,
228
236
  ]);
229
237
 
230
238
  return (
@@ -47,10 +47,11 @@ const QuickAction = memo(
47
47
  interface ChatInfoProfileProps {
48
48
  conversation?: ConversationItem;
49
49
  onPressSearch?: () => void;
50
+ isNotInGroup?: boolean;
50
51
  }
51
52
 
52
53
  const ChatInfoProfile = memo(
53
- ({ conversation, onPressSearch }: ChatInfoProfileProps) => {
54
+ ({ conversation, onPressSearch, isNotInGroup }: ChatInfoProfileProps) => {
54
55
  const { faceURL, showName = '' } = conversation ?? {};
55
56
 
56
57
  const isMuted =
@@ -122,17 +123,21 @@ const ChatInfoProfile = memo(
122
123
  background={KColors.palette.gray.w25}
123
124
  iconColor={KColors.gray.dark}
124
125
  />
125
- <QuickAction
126
- iconName={isMuted ? 'bell-b' : 'bell-off-o'}
127
- label={
128
- isMuted ? trans('chat:unmute_action') : trans('chat:mute_action')
129
- }
130
- background={
131
- isMuted ? KColors.primary.normal : KColors.palette.gray.w25
132
- }
133
- iconColor={isMuted ? KColors.white : KColors.gray.dark}
134
- onPress={handleNotificationPress}
135
- />
126
+ <KContainer.VisibleView visible={!isNotInGroup}>
127
+ <QuickAction
128
+ iconName={isMuted ? 'bell-b' : 'bell-off-o'}
129
+ label={
130
+ isMuted
131
+ ? trans('chat:unmute_action')
132
+ : trans('chat:mute_action')
133
+ }
134
+ background={
135
+ isMuted ? KColors.primary.normal : KColors.palette.gray.w25
136
+ }
137
+ iconColor={isMuted ? KColors.white : KColors.gray.dark}
138
+ onPress={handleNotificationPress}
139
+ />
140
+ </KContainer.VisibleView>
136
141
  </KContainer.View>
137
142
  </KContainer.View>
138
143
  );
@@ -1,4 +1,4 @@
1
- import { memo, useCallback, useEffect, useRef } from 'react';
1
+ import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
2
2
  import { DeviceEventEmitter } from 'react-native';
3
3
 
4
4
  import { useKeyboardScrollToEnd } from '@legendapp/list/keyboard';
@@ -40,6 +40,7 @@ import {
40
40
  } from '../../hooks/conversation/useGetConversationDetail';
41
41
  import {
42
42
  GroupJoinSource,
43
+ PeerType,
43
44
  type MessageItem,
44
45
  } from '@droppii/openim-rn-client-sdk';
45
46
  import { useMessages } from '../../hooks/message/useMessages';
@@ -51,7 +52,12 @@ import { useSingleAccessGuard } from '../../hooks/conversation/useSingleAccessGu
51
52
  import { useIsJoinedGroup } from '../../hooks/useIsJoinedGroup';
52
53
  import { UIUtils } from '../../utils/UIUtils';
53
54
  import { trans } from '../../translation';
54
- import { getGroupIdFromConversationId } from '../../utils/conversation';
55
+ import {
56
+ getGroupIdFromConversationId,
57
+ isConversationParticipant,
58
+ isSingleConversationId,
59
+ } from '../../utils/conversation';
60
+ import { useUnreadIndicator } from './hooks/useUnreadIndicator';
55
61
  import { ChatDetailLoading } from './components/ChatDetailLoading';
56
62
 
57
63
  interface ChatDetailScreenProps extends Omit<
@@ -111,6 +117,25 @@ const ChatDetail = memo(
111
117
  []
112
118
  );
113
119
 
120
+ // Bot from the conversation itself: ChatDetailContext's flag is derived
121
+ // from an id this screen sets in an effect, so it's still false on the
122
+ // first effect pass.
123
+ const isBotThread = conversation?.peerType === PeerType.Bot;
124
+ // 1v1: only a participant may look up / mark read. Groups: the gate's
125
+ // `isReady` already enforced join/access before this screen mounted.
126
+ const accessConfirmed =
127
+ !!currentUser?.userID &&
128
+ (!isSingleConversationId(conversationId) ||
129
+ isConversationParticipant(conversationId, currentUser.userID));
130
+ // Must stay above useMessages (see useUnreadDeeplinkCheck).
131
+ const { unreadToken, unreadTokenRef, markChildNavigation } =
132
+ useUnreadIndicator({
133
+ conversationId,
134
+ isBotConversation: isBotThread,
135
+ targetMessageId,
136
+ accessConfirmed,
137
+ });
138
+
114
139
  const {
115
140
  messages,
116
141
  initLoading,
@@ -222,9 +247,34 @@ const ChatDetail = memo(
222
247
  });
223
248
  }, [currentUser, unpinMessage, customMessageItemFactory]);
224
249
 
250
+ // Library-initiated child navigation keeps the unread indicator across the
251
+ // blur it causes; markChildNavigation must run before the callback does.
225
252
  const handlePressMenu = useCallback(() => {
253
+ markChildNavigation();
226
254
  onPressMenu?.(conversationId);
227
- }, [conversationId, onPressMenu]);
255
+ }, [conversationId, onPressMenu, markChildNavigation]);
256
+
257
+ const handlePressSearch = useMemo(
258
+ () =>
259
+ onPressSearch
260
+ ? (id: string) => {
261
+ markChildNavigation();
262
+ onPressSearch(id);
263
+ }
264
+ : undefined,
265
+ [onPressSearch, markChildNavigation]
266
+ );
267
+
268
+ const handlePressAddMember = useMemo(
269
+ () =>
270
+ onPressAddMember
271
+ ? () => {
272
+ markChildNavigation();
273
+ onPressAddMember();
274
+ }
275
+ : undefined,
276
+ [onPressAddMember, markChildNavigation]
277
+ );
228
278
 
229
279
  const handlePressJoinGroup = useCallback(() => {
230
280
  if (!conversation?.groupID) return;
@@ -248,8 +298,8 @@ const ChatDetail = memo(
248
298
  targetUserId={targetUserId}
249
299
  showAddMember={resolvedShowAddMember}
250
300
  onBack={onBack ?? handleBackDefault}
251
- onPressSearch={onPressSearch}
252
- onPressAddMember={onPressAddMember}
301
+ onPressSearch={handlePressSearch}
302
+ onPressAddMember={handlePressAddMember}
253
303
  onPressMenu={handlePressMenu}
254
304
  onPressAvatar={handlePressMenu}
255
305
  />
@@ -283,6 +333,9 @@ const ChatDetail = memo(
283
333
  dismissAreaTouchProps={dismissAreaTouchProps}
284
334
  isComposerPanelOpen={isComposerPanelOpen}
285
335
  onDismissAreaScrollBeginDrag={notifyScrollBeginDrag}
336
+ unreadToken={unreadToken}
337
+ unreadTokenRef={unreadTokenRef}
338
+ deeplinkTargetId={targetMessageId || undefined}
286
339
  />
287
340
  </TimelinePressGuardProvider>
288
341