@antzsoft/chat-core 1.4.0 → 1.4.2

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/README.md CHANGED
@@ -43,7 +43,7 @@ npm install @antzsoft/chat-core
43
43
  | Capability | What the SDK provides |
44
44
  |---|---|
45
45
  | Authentication | Login, register, logout, token refresh (automatic on 401) |
46
- | Conversations | List, create (group/DM), update, delete, mute, pin, leave, manage members |
46
+ | Conversations | List, create (group/DM), update, delete, mute, pin, mark unread, leave, manage members |
47
47
  | Messages | Send, edit, delete, react, star, pin, search, paginate, @mention |
48
48
  | Mentions | Group @mentions with `@all` (admin-gated); token parse/build/render helpers; mention pierces mute |
49
49
  | File uploads | Presigned URL pipeline — request URL → upload binary (multipart POST for S3/local, PUT for Azure) → confirm. Files ≥ 10 MB on S3 or local use chunked multipart (parallel parts → complete). |
@@ -764,6 +764,7 @@ import { messagesApi } from '@antzsoft/chat-core';
764
764
  | `pin` | `(messageId: string) => Promise<Message>` | Pin a message. |
765
765
  | `unpin` | `(messageId: string) => Promise<Message>` | Unpin a message. |
766
766
  | `getPinned` | `(conversationId: string) => Promise<Message[]>` | List pinned messages in a conversation. |
767
+ | `forward` | `(messageId: string, targetConversationIds: string[]) => Promise<ForwardResult[]>` | Forward a message into one or more conversations (v1.4.2+). Max `MAX_FORWARD_TARGETS` (5) targets per call, reduced to 1 if the source message has `forwardDepth >= 5`. Each target is independent — check `success`/`error` per entry in the returned array. No hard cap on forward count. |
767
768
 
768
769
  > **Delete permissions** — `delete()` (for everyone) requires either: the message belongs to the current user AND was sent within the delete window, OR the current user is a group admin. The server default window is **216,000 s (60 hours)** when `conversation.settings.messageConfig.deleteWindowSeconds` is not set. DMs have no admin role — only the sender can delete for everyone in a DM. `deleteForMe()` is always allowed for any message. See the [integration guide — Edit & Delete section](docs/integration-guide.html#step-edit) for a ready-to-use `getDeleteOptions()` helper.
769
770
 
@@ -812,6 +813,57 @@ await messagesApi.send('conv-abc', {
812
813
  const results = await messagesApi.search({ query: 'deployment', conversationId: 'conv-abc' });
813
814
  ```
814
815
 
816
+ #### Forward Message (v1.4.2+)
817
+
818
+ Forwarding creates an **independent copy** of a message in each target conversation — never a shared row. Each copy gets its own ID, sequence number, read receipts, and delete lifecycle; deleting one copy never affects another, and none of them affect the original.
819
+
820
+ | Field / method | Type | Description |
821
+ |---|---|---|
822
+ | `messagesApi.forward(messageId, targetConversationIds)` | `Promise<ForwardResult[]>` | Forwards into up to `MAX_FORWARD_TARGETS` (5) conversations in one call — fewer if the source message is highly forwarded, see Limits below. |
823
+ | `Message.forwardedFrom` | `MessageForwardReference \| undefined` | Present on a message created via forwarding. |
824
+ | `MAX_FORWARD_TARGETS` | `number` | `5` — matches the server's normal-case cap. |
825
+
826
+ **Limits:**
827
+
828
+ | Limit | Value | Behavior when exceeded |
829
+ |---|---|---|
830
+ | Targets per call (normal) | 5 (`MAX_FORWARD_TARGETS`) | Client: `ForwardPicker` disables further selection. Server: request rejected with a validation error if bypassed. |
831
+ | Targets per call — highly-forwarded content (`forwardDepth >= 5`) | **1** | Server silently truncates to the first target and reports every dropped target back as an explicit `{ success: false, error }` entry in `ForwardResult[]` — never a validation error, never a fully blocked forward. Matches WhatsApp's actual behavior: reduced fan-out, not a forwarding ban. |
832
+ | Forward API rate limit | 20 calls/min per user | `429` if exceeded (server-side `@Throttle`, not currently surfaced as a distinct client-side pre-check). |
833
+ | Forward-count hard cap | **None** | A message can be forwarded any number of times — there is no "forwarded too many times, blocked" state. Only fan-out width is reduced at high `forwardDepth`; `forwardDepth` itself only ever drives the "Forwarded many times" label. |
834
+
835
+ Because dropped targets from the highly-forwarded cap come back as regular `ForwardResult` failures, existing "Forwarded to N/M chats" UI (see the example below) needs no special-casing to handle it correctly.
836
+
837
+ ```typescript
838
+ interface MessageForwardReference {
839
+ originalMessageId: string; // one hop back only — never the root of a longer chain
840
+ originalConversationId: string; // one hop back only
841
+ originalSenderId: string; // one hop back only — NOT the original author past 1 hop
842
+ forwardDepth: number; // accumulates across the whole chain
843
+ }
844
+
845
+ interface ForwardResult {
846
+ conversationId: string;
847
+ success: boolean;
848
+ message?: Message; // present when success is true
849
+ error?: string; // present when success is false
850
+ }
851
+ ```
852
+
853
+ ```typescript
854
+ const results = await messagesApi.forward(messageId, [convA, convB, convC]);
855
+ const failed = results.filter(r => !r.success);
856
+ if (failed.length > 0) {
857
+ console.warn(`Forward failed for ${failed.length}/${results.length} conversations`, failed);
858
+ }
859
+ ```
860
+
861
+ **Why only one hop of lineage is kept.** `forwardedFrom` always points at the *immediate* message being forwarded, never the ultimate original — matching WhatsApp: if you forward a message that was itself already forwarded, the new copy's `originalMessageId`/`originalSenderId` reference that intermediate copy, not the very first message in the chain. This is intentional: it keeps the true original author untraceable after more than one hop (privacy), while `forwardDepth` still accumulates across the whole chain. `forwardDepth` drives two things, both purely about slowing spread, never about blocking it: the "Forwarded many times" badge (client convention: `forwardDepth >= 4`), and server-side, the reduced 1-target fan-out cap once `forwardDepth >= 5` (see Limits above). There is no API to walk a message's full forward history — only the immediate parent is ever resolvable, and there is no forward-count ceiling that ever blocks forwarding outright.
862
+
863
+ **Attachments are never re-uploaded.** A forwarded attachment reuses the same underlying storage object as the source message — only the message row referencing it is new. This is transparent to SDK consumers; `forward()` handles it server-side.
864
+
865
+ **Rendering:** show a "Forwarded" (or "Forwarded many times" at `forwardDepth >= 4`) label when `message.forwardedFrom` is present — do not render a sender name or content preview for it (unlike `replyTo`), since the message's own `content` already holds what was forwarded. The built-in `MessageItem` component in both `@antzsoft/chat-web-sdk` and `@antzsoft/chat-rn-sdk` already renders this label and exposes a "Forward" action in the message menu — no extra wiring needed if you're using the prebuilt UI.
866
+
815
867
  #### Jump to first unread message
816
868
 
817
869
  Use `direction: 'after'` with the user's `lastReadMessageId` as the cursor to fetch only the unread messages. This powers a scroll-to-first-unread experience with an "↑ Unread messages" divider.
@@ -945,6 +997,8 @@ import { conversationsApi } from '@antzsoft/chat-core';
945
997
  | `unmute` | `(conversationId: string) => Promise<void>` | Unmute a conversation. |
946
998
  | `pin` | `(conversationId: string) => Promise<void>` | Pin a conversation to the top of the list. Max 5 pins — server returns `400` if the limit is reached. |
947
999
  | `unpin` | `(conversationId: string) => Promise<void>` | Unpin a conversation. |
1000
+ | `markUnread` | `(conversationId: string) => Promise<void>` | Manually flag a conversation as unread (`Conversation.isManuallyUnread` becomes `true`), independent of `unreadCount`. |
1001
+ | `markRead` | `(conversationId: string) => Promise<void>` | Clear the manual unread flag. Also cleared automatically whenever the conversation is opened/read through the normal mark-as-read flow. |
948
1002
  | `leave` | `(conversationId: string, andDelete?: boolean) => Promise<void>` | Leave a group conversation. Pass `andDelete: true` to also hide it from the caller's list in one atomic operation ("Exit and Delete"). When the last admin calls `leave()`, the server automatically promotes the longest-standing active member to admin before completing the exit — no client action required. |
949
1003
  | `getMembers` | `(conversationId: string) => Promise<User[]>` | Fetch full user profiles for all participants. |
950
1004
 
@@ -1165,6 +1219,44 @@ async function onForeground() {
1165
1219
 
1166
1220
  > **Using `@antzsoft/chat-web-sdk` or `@antzsoft/chat-rn-sdk`?** You don't need any of this — `useConversations()` handles socket subscriptions internally. Just sum `conversations.reduce((s, c) => s + (c.unreadCount ?? 0), 0)` and it updates automatically.
1167
1221
 
1222
+ #### Mark as Unread (v1.4.1+)
1223
+
1224
+ `isManuallyUnread` is a separate flag from `unreadCount` — it lets a user re-flag a conversation they've already read so it stands out again in the list, without fabricating unread messages or moving the read-receipt cursor. This is the same "mark as unread" behavior as WhatsApp/Telegram: a dot indicator, not a count.
1225
+
1226
+ | Field / method | Type | Description |
1227
+ |---|---|---|
1228
+ | `Conversation.isManuallyUnread` | `boolean \| undefined` | `true` once flagged; `false`/absent otherwise. Independent of `unreadCount` — both can be `true`/`>0` at once, or `isManuallyUnread` can be `true` while `unreadCount` is `0`. |
1229
+ | `conversationsApi.markUnread(id)` | `Promise<void>` | Sets the flag. |
1230
+ | `conversationsApi.markRead(id)` | `Promise<void>` | Clears the flag. |
1231
+
1232
+ ```typescript
1233
+ // Flag a fully-read conversation as unread
1234
+ await conversationsApi.markUnread(conversationId);
1235
+
1236
+ // Clear it manually (rarely needed — see auto-clear below)
1237
+ await conversationsApi.markRead(conversationId);
1238
+
1239
+ // Render: numbered badge takes priority; fall back to a plain dot
1240
+ const conv = await conversationsApi.get(conversationId);
1241
+ if ((conv.unreadCount ?? 0) > 0) {
1242
+ showBadge(conv.unreadCount);
1243
+ } else if (conv.isManuallyUnread) {
1244
+ showDot();
1245
+ }
1246
+ ```
1247
+
1248
+ **Auto-clears on read.** Opening the conversation — anything that runs the normal mark-as-read flow (socket `mark_read`, or the REST notification-catchup path) — clears `isManuallyUnread` server-side automatically, same as WhatsApp. You do not need to call `markRead()` yourself after the user opens the chat; it's only for the explicit "un-flag without opening" action (e.g. an X button on the dot).
1249
+
1250
+ **Live sync across devices.** Unlike `mute`/`pin` (which currently only take effect on the next fetch), toggling `isManuallyUnread` emits a `conversation_updated` socket event to the caller's other connected sessions immediately:
1251
+
1252
+ ```typescript
1253
+ socket?.on('conversation_updated', (conv) => {
1254
+ // conv.isManuallyUnread reflects the latest state, pushed live
1255
+ });
1256
+ ```
1257
+
1258
+ **Web/RN SDK hooks expose named mutations:** `markUnread`, `markRead` (both SDKs) — cache updated optimistically, no manual invalidation needed. The built-in `ConversationList` component already renders the dot and exposes the toggle from its existing Pin/Mute menu.
1259
+
1168
1260
  #### Clear / Delete Chat (v1.2.6+)
1169
1261
 
1170
1262
  `conversationsApi.delete(conversationId)` hides a conversation from the caller's list. Any participant can call it — no admin role required. Other participants are completely unaffected.
@@ -2845,6 +2937,7 @@ interface Conversation {
2845
2937
  isPinned?: boolean;
2846
2938
  isMuted?: boolean;
2847
2939
  mutedUntil?: string;
2940
+ isManuallyUnread?: boolean; // v1.4.1+ — manually flagged unread, independent of unreadCount
2848
2941
  }
2849
2942
 
2850
2943
  interface ConversationSettings {
@@ -3122,6 +3215,24 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
3122
3215
 
3123
3216
  ## Changelog
3124
3217
 
3218
+ ### v1.4.1
3219
+
3220
+ - **New: manual "mark as unread."** A conversation can now be flagged unread independently of `unreadCount` — the same UX as WhatsApp/Telegram's "mark as unread": re-flag a conversation you've already read so it stands out again, without fabricating unread messages.
3221
+ - **New field:** `Conversation.isManuallyUnread?: boolean`. Present on every conversation object returned from `conversationsApi.list/get`, and on `conversation_created`/`conversation_updated` socket payloads that carry a full conversation DTO. `false`/absent for a conversation that has never been manually flagged.
3222
+ - **New API methods:** `conversationsApi.markUnread(conversationId)` and `conversationsApi.markRead(conversationId)` — both `Promise<void>`, following the same shape as `pin`/`unpin`.
3223
+ - **Auto-clears on read.** Opening the conversation (anything that calls the existing mark-as-read flow — socket `mark_read` or the REST notification-catchup path) clears the flag server-side automatically, same as WhatsApp: you don't have to manually un-flag it after reading.
3224
+ - **Live sync.** Toggling the flag emits a `conversation_updated` event to the caller's other connected devices/tabs, so mark-unread/mark-read state stays in sync across sessions immediately — unlike `mute`/`pin`, which currently only take effect on next fetch.
3225
+
3226
+ **Backward compatible, additive-only.** `isManuallyUnread` is optional; an older SDK against a server with this change simply ignores the extra field. **No integration changes required** unless you want to surface the new flag/actions in your own conversation list UI (the RN and web SDKs' built-in `ConversationList` already do, see their changelogs).
3227
+
3228
+ ### v1.4.0
3229
+
3230
+ - **Fix: stale transit encryption key after token refresh, causing continuous `"Transit decryption failed for event: user_online"` spam.** `reconnectSocket()` called `socket.connect()` to apply a refreshed token while preserving the transit session — but socket.io-client's `connect()` is a no-op on an already-connected socket. A token refresh fires while the user is actively on a chat screen (socket still connected), so the new auth was never actually sent; the socket kept running on the stale token until the server eventually dropped it, and the disconnect handler then cleared the transit session, desyncing the client's key from the server's.
3231
+
3232
+ **Fix:** in the transit branch of `reconnectSocket()`, force a real transport re-cycle (disconnect → connect) so the fresh auth (token + `transitSessionId`) is actually sent and the server re-links the *same* session (same key), instead of silently continuing on stale auth. A new internal `_preservingSession` guard stops the disconnect handler from wiping the key being deliberately carried forward. Non-transit and already-disconnected paths are unchanged.
3233
+
3234
+ **Backward compatible.** Only affects transit-encryption deployments that refresh auth tokens while the socket is connected. **No integration changes required** — the fix is entirely internal to `reconnectSocket()`.
3235
+
3125
3236
  ### v1.3.9
3126
3237
 
3127
3238
  - **New: group @mentions.** Tag members in a group message; a mentioned user is notified even if they muted the group (the mention pierces mute). Mentions are stored inline in the message text as self-describing tokens `@[DisplayName](userId)` (and `@[all](all)` for @all), plus a flat, denormalized `mentions: string[]` array on the message for fan-out and "who was mentioned" lookups. No offsets are stored — the token is self-locating and survives edits.
@@ -0,0 +1,7 @@
1
+ import {
2
+ useChatStore
3
+ } from "./chunk-UIYJAOGL.js";
4
+ export {
5
+ useChatStore
6
+ };
7
+ //# sourceMappingURL=chat.store-UVTDBPEC.js.map
@@ -9,11 +9,12 @@ var useChatStore = create((set) => ({
9
9
  lastSeen: {},
10
10
  replyingTo: null,
11
11
  editingMessage: null,
12
+ forwardingMessage: null,
12
13
  isSidebarOpen: true,
13
14
  isGroupInfoOpen: false,
14
15
  isStarredPanelOpen: false,
15
16
  messageInfoId: null,
16
- setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
17
+ setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),
17
18
  setPendingTarget: (target) => set({ pendingTarget: target }),
18
19
  addTypingUser: (conversationId, user) => set((state) => {
19
20
  const existing = state.typingUsers[conversationId] ?? [];
@@ -45,6 +46,7 @@ var useChatStore = create((set) => ({
45
46
  }),
46
47
  setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
47
48
  setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
49
+ setForwardingMessage: (message) => set({ forwardingMessage: message }),
48
50
  toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
49
51
  setSidebarOpen: (open) => set({ isSidebarOpen: open }),
50
52
  toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
@@ -57,4 +59,4 @@ var useChatStore = create((set) => ({
57
59
  export {
58
60
  useChatStore
59
61
  };
60
- //# sourceMappingURL=chunk-EOL5B7GS.js.map
62
+ //# sourceMappingURL=chunk-UIYJAOGL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stores/chat.store.ts"],"sourcesContent":["import { create } from 'zustand';\nimport type { Message } from '../types/index.js';\n\ninterface TypingUser {\n userId: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface LastReadEntry {\n messageId: string;\n readAt: string;\n}\n\ninterface ChatState {\n activeConversationId: string | null;\n pendingTarget: { conversationId: string; messageId: string } | null;\n typingUsers: Record<string, TypingUser[]>;\n onlineUsers: string[];\n /** keyed by conversationId — current user's last read pointer per conversation */\n lastRead: Record<string, LastReadEntry>;\n /** keyed by userId — last seen timestamp for each user */\n lastSeen: Record<string, string>;\n replyingTo: Message | null;\n editingMessage: Message | null;\n /** Message staged in the forward picker, null = picker closed. Unlike replyingTo,\n * this does not fold into the next composer send — forwarding targets a different\n * conversation and needs its own conversation-picker UI. */\n forwardingMessage: Message | null;\n isSidebarOpen: boolean;\n isGroupInfoOpen: boolean;\n isStarredPanelOpen: boolean;\n /** messageId currently shown in the Message Info panel, null = closed */\n messageInfoId: string | null;\n\n setActiveConversation: (id: string | null) => void;\n setPendingTarget: (target: { conversationId: string; messageId: string } | null) => void;\n addTypingUser: (conversationId: string, user: TypingUser) => void;\n removeTypingUser: (conversationId: string, userId: string) => void;\n setUserOnline: (userId: string) => void;\n setUserOffline: (userId: string) => void;\n setOnlineUsers: (userIds: string[]) => void;\n setLastRead: (conversationId: string, messageId: string, readAt: string) => void;\n setLastSeen: (userId: string, lastSeenAt: string | null) => void;\n setReplyingTo: (message: Message | null) => void;\n setEditingMessage: (message: Message | null) => void;\n setForwardingMessage: (message: Message | null) => void;\n toggleSidebar: () => void;\n setSidebarOpen: (open: boolean) => void;\n toggleGroupInfo: () => void;\n setGroupInfoOpen: (open: boolean) => void;\n toggleStarredPanel: () => void;\n setStarredPanelOpen: (open: boolean) => void;\n setMessageInfoId: (id: string | null) => void;\n}\n\nexport const useChatStore = create<ChatState>((set) => ({\n activeConversationId: null,\n pendingTarget: null,\n typingUsers: {},\n onlineUsers: [],\n lastRead: {},\n lastSeen: {},\n replyingTo: null,\n editingMessage: null,\n forwardingMessage: null,\n isSidebarOpen: true,\n isGroupInfoOpen: false,\n isStarredPanelOpen: false,\n messageInfoId: null,\n\n setActiveConversation: (id) =>\n set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),\n\n setPendingTarget: (target) => set({ pendingTarget: target }),\n\n addTypingUser: (conversationId, user) =>\n set((state) => {\n const existing = state.typingUsers[conversationId] ?? [];\n const deduped = existing.filter((u) => u.userId !== user.userId);\n return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };\n }),\n\n removeTypingUser: (conversationId, userId) =>\n set((state) => ({\n typingUsers: {\n ...state.typingUsers,\n [conversationId]: (state.typingUsers[conversationId] ?? []).filter(\n (u) => u.userId !== userId,\n ),\n },\n })),\n\n setUserOnline: (userId) =>\n set((state) => ({\n onlineUsers: state.onlineUsers.includes(userId)\n ? state.onlineUsers\n : [...state.onlineUsers, userId],\n })),\n\n setUserOffline: (userId) =>\n set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),\n\n setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),\n\n setLastRead: (conversationId, messageId, readAt) =>\n set((state) => ({\n lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } },\n })),\n\n setLastSeen: (userId, lastSeenAt) =>\n set((state) => {\n if (lastSeenAt === null) {\n const { [userId]: _, ...rest } = state.lastSeen;\n return { lastSeen: rest };\n }\n return { lastSeen: { ...state.lastSeen, [userId]: lastSeenAt } };\n }),\n\n setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),\n\n setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),\n\n setForwardingMessage: (message) => set({ forwardingMessage: message }),\n\n toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),\n setSidebarOpen: (open) => set({ isSidebarOpen: open }),\n\n toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),\n setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),\n\n toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),\n setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),\n\n setMessageInfoId: (id: string | null) => set({ messageInfoId: id }),\n}));\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -49,11 +49,12 @@ var init_chat_store = __esm({
49
49
  lastSeen: {},
50
50
  replyingTo: null,
51
51
  editingMessage: null,
52
+ forwardingMessage: null,
52
53
  isSidebarOpen: true,
53
54
  isGroupInfoOpen: false,
54
55
  isStarredPanelOpen: false,
55
56
  messageInfoId: null,
56
- setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
57
+ setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),
57
58
  setPendingTarget: (target) => set({ pendingTarget: target }),
58
59
  addTypingUser: (conversationId, user) => set((state) => {
59
60
  const existing = state.typingUsers[conversationId] ?? [];
@@ -85,6 +86,7 @@ var init_chat_store = __esm({
85
86
  }),
86
87
  setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
87
88
  setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
89
+ setForwardingMessage: (message) => set({ forwardingMessage: message }),
88
90
  toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
89
91
  setSidebarOpen: (open) => set({ isSidebarOpen: open }),
90
92
  toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
@@ -106,6 +108,7 @@ __export(src_exports, {
106
108
  AntzChatPermissionError: () => AntzChatPermissionError,
107
109
  AntzChatServerError: () => AntzChatServerError,
108
110
  AntzChatValidationError: () => AntzChatValidationError,
111
+ MAX_FORWARD_TARGETS: () => MAX_FORWARD_TARGETS,
109
112
  MENTION_ALL_ID: () => MENTION_ALL_ID,
110
113
  appConfigApi: () => appConfigApi,
111
114
  authApi: () => authApi,
@@ -1010,6 +1013,7 @@ function resolveSystemMessageText(message, currentUserId) {
1010
1013
  }
1011
1014
 
1012
1015
  // src/api/messages.ts
1016
+ var MAX_FORWARD_TARGETS = 5;
1013
1017
  var messagesApi = {
1014
1018
  async list(conversationId, params = {}) {
1015
1019
  const { cursor, direction, ...rest } = params;
@@ -1103,6 +1107,19 @@ var messagesApi = {
1103
1107
  async getReceipts(messageId) {
1104
1108
  const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
1105
1109
  return data;
1110
+ },
1111
+ /**
1112
+ * Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS
1113
+ * per call, also enforced server-side). Each target is independent — one failing
1114
+ * (e.g. no longer a participant) does not block the others; check `success`/`error`
1115
+ * per entry in the returned array.
1116
+ */
1117
+ async forward(messageId, targetConversationIds) {
1118
+ const { data } = await getApiClient().post(
1119
+ `/messages/${messageId}/forward`,
1120
+ { targetConversationIds }
1121
+ );
1122
+ return data;
1106
1123
  }
1107
1124
  };
1108
1125
 
@@ -1216,6 +1233,12 @@ var conversationsApi = {
1216
1233
  async unpin(conversationId) {
1217
1234
  await getApiClient().post(`/conversations/${conversationId}/unpin`);
1218
1235
  },
1236
+ async markUnread(conversationId) {
1237
+ await getApiClient().post(`/conversations/${conversationId}/unread`);
1238
+ },
1239
+ async markRead(conversationId) {
1240
+ await getApiClient().post(`/conversations/${conversationId}/unread/clear`);
1241
+ },
1219
1242
  async leave(conversationId, andDelete) {
1220
1243
  const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
1221
1244
  await getApiClient().post(url);
@@ -2090,6 +2113,7 @@ var AntzChatClient = class {
2090
2113
  AntzChatPermissionError,
2091
2114
  AntzChatServerError,
2092
2115
  AntzChatValidationError,
2116
+ MAX_FORWARD_TARGETS,
2093
2117
  MENTION_ALL_ID,
2094
2118
  appConfigApi,
2095
2119
  authApi,