@manonero/chat-client-sdk 1.0.0-beta.11 → 1.0.0-beta.12

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
@@ -85,6 +85,11 @@ import { ChatApiError } from '@manonero/chat-client-sdk'; // class — dùng imp
85
85
  import { TypedEventEmitter } from '@manonero/chat-client-sdk';
86
86
  import type { Unsubscribe } from '@manonero/chat-client-sdk';
87
87
 
88
+ // computeUnreadCount — tính unread badge từ ConversationDto / ConversationListItemDto
89
+ // hoặc bất kỳ object nào có { messageSequence, lastReadSequence, markedUnread }
90
+ import { computeUnreadCount } from '@manonero/chat-client-sdk';
91
+ import type { UnreadCountFields } from '@manonero/chat-client-sdk';
92
+
88
93
  // HttpClient — tự xây dựng API module bổ sung theo cùng pattern
89
94
  import { HttpClient } from '@manonero/chat-client-sdk';
90
95
  import type { HttpClientOptions } from '@manonero/chat-client-sdk';
@@ -490,7 +495,7 @@ await client.conversations.delete('conv-id');
490
495
 
491
496
  ### `leaveGroup(id: string): Promise<void>`
492
497
 
493
- Rời cuộc hội thoại **Group** một cách tường minh. Trả về `403 Forbidden` nếu conversation không phải Group.
498
+ Rời cuộc hội thoại **Group** một cách tường minh. Trả về `400 Bad Request` nếu conversation không phải Group.
494
499
 
495
500
  > Dùng `leaveGroup()` khi cần ngữ nghĩa Group-only với phản hồi lỗi rõ ràng cho non-Group. Dùng `delete()` khi muốn xử lý cả Group lẫn OneToOne/Self trong cùng một flow.
496
501
 
@@ -596,11 +601,19 @@ interface ConversationDto {
596
601
  ownerId: string;
597
602
  lastMessageAt?: string | null; // ISO 8601, null khi chưa có tin nhắn
598
603
  lastMessageId?: string | null; // null khi chưa có tin nhắn
604
+ messageSequence: number; // Sequence tin nhắn mới nhất trong conversation
599
605
  createdAt: string; // ISO 8601
600
606
  updatedAt: string; // ISO 8601
607
+ selfReadState: SelfReadState | null; // null khi caller không phải active participant
601
608
  participants: ConversationParticipantDto[];
602
609
  }
603
610
 
611
+ // Read state của chính caller — dùng để tính unread badge phía client.
612
+ interface SelfReadState {
613
+ lastReadSequence: number;
614
+ markedUnread: boolean;
615
+ }
616
+
604
617
  interface ConversationParticipantDto {
605
618
  participantId: string;
606
619
  joinedAt: string; // ISO 8601
@@ -610,7 +623,6 @@ interface ConversationParticipantDto {
610
623
  pinnedAt?: string | null; // ISO 8601
611
624
  lastReadMessageId?: string | null;
612
625
  lastReadAt?: string | null; // ISO 8601
613
- unreadCount: number;
614
626
  }
615
627
 
616
628
  interface ConversationListItemDto {
@@ -620,13 +632,43 @@ interface ConversationListItemDto {
620
632
  avatar?: MediaReference | null;
621
633
  lastMessageAt?: string | null; // ISO 8601
622
634
  lastMessageId?: string | null;
635
+ messageSequence: number; // Sequence tin nhắn mới nhất
636
+ lastReadSequence: number; // Sequence đã đọc của caller (0 nếu chưa đọc)
637
+ markedUnread: boolean; // Cờ mark-as-unread của caller
623
638
  lastMessage?: MessageDto | null; // Preview tin nhắn cuối, null khi chưa có tin nhắn
624
639
  isPinned: boolean;
625
640
  pinnedAt?: string | null; // ISO 8601, null khi chưa pin
626
- unreadCount: number;
627
641
  participantCount: number;
628
642
  }
643
+ ```
644
+
645
+ > **Unread badge tính tại client.** Server không trả số tin chưa đọc được tính sẵn — thay vào đó gửi 3 field (`messageSequence`, `lastReadSequence`, `markedUnread`) để client tự tính và cập nhật từ các SignalR event `UnreadCountChanged` / `ReadStatusChanged` mà không cần refetch. Công thức:
646
+ >
647
+ > ```
648
+ > unread = max(messageSequence - lastReadSequence, markedUnread ? 1 : 0)
649
+ > ```
650
+ >
651
+ > SDK export sẵn helper `computeUnreadCount` cài đặt đúng công thức trên (bao gồm clamp về 0 khi race condition khiến `lastReadSequence > messageSequence`). Helper nhận cả `ConversationListItemDto`, `ConversationDto`, hoặc bất kỳ object nào có 3 field `{ messageSequence, lastReadSequence, markedUnread }`:
652
+ >
653
+ > ```ts
654
+ > import { computeUnreadCount } from '@manonero/chat-client-sdk';
655
+ >
656
+ > // Từ list item (shape phẳng)
657
+ > const unreadList = computeUnreadCount(item);
658
+ >
659
+ > // Từ ConversationDto (selfReadState lồng bên trong); trả về 0 nếu
660
+ > // selfReadState === null (caller không phải active participant)
661
+ > const unread = computeUnreadCount(conv);
662
+ >
663
+ > // Từ cache nội bộ của bạn
664
+ > const unreadFromCache = computeUnreadCount({
665
+ > messageSequence: cached.messageSequence,
666
+ > lastReadSequence: cached.lastReadSequence,
667
+ > markedUnread: cached.markedUnread,
668
+ > });
669
+ > ```
629
670
 
671
+ ```ts
630
672
  // Media listing (getMedia)
631
673
  type ConversationMediaKindFilter = 'all' | 'attachment' | 'link';
632
674
  type ConversationMediaKind = 'Attachment' | 'Link';
@@ -1488,14 +1530,14 @@ await client.realtime.notifications.unsubscribeFromPresence(['user-1']);
1488
1530
  | `presenceChanged` | `PresenceChangedDto` | Thay đổi trạng thái online |
1489
1531
  | `newMessageNotification` | `NewMessageNotificationDto` | Có tin nhắn mới (thông báo cho tất cả participants trừ người gửi) |
1490
1532
  | `mentionedNotification` | `MentionedNotificationDto` | Bị mention trong tin nhắn |
1491
- | `unreadCountChanged` | `UnreadCountChangedDto` | Số tin chưa đọc thay đổi |
1533
+ | `unreadCountChanged` | `UnreadCountChangedDto` | `MessageSequence` của conversation tăng (có tin mới từ user khác) — client tự tính lại badge |
1492
1534
  | `conversationCreated` | `ConversationCreatedDto` | Cuộc hội thoại mới được tạo (hoặc bị thêm vào) |
1493
1535
  | `conversationUpdated` | `ConversationUpdatedDto` | Metadata cuộc hội thoại thay đổi |
1494
1536
  | `participantJoined` | `ParticipantJoinedDto` | Thành viên mới tham gia |
1495
1537
  | `participantLeft` | `ParticipantLeftDto` | Thành viên rời nhóm |
1496
1538
  | `conversationPinned` | `ConversationPinnedDto` | Cuộc hội thoại được pin |
1497
1539
  | `conversationUnpinned` | `ConversationUnpinnedDto` | Cuộc hội thoại được unpin |
1498
- | `readStatusChanged` | `ReadStatusChangedDto` | Trạng thái đọc thay đổi |
1540
+ | `readStatusChanged` | `ReadStatusChangedDto` | Trạng thái đọc thay đổi (do chính user gọi `POST/DELETE /api/conversations/{id}/read`) — chỉ gửi tới owner |
1499
1541
  | `conversationDeleted` | `ConversationDeletedDto` | Conversation bị ẩn/xóa khỏi danh sách (chỉ OneToOne & Self, chỉ user thực hiện) |
1500
1542
  | `conversationRestored` | `ConversationRestoredDto` | Conversation bị ẩn được khôi phục tự động do có tin nhắn mới (chỉ OneToOne & Self) |
1501
1543
  | `error` | `HubErrorDto` | Lỗi từ server |
@@ -1542,9 +1584,13 @@ interface MentionedNotificationDto {
1542
1584
  sentAt: string; // ISO 8601
1543
1585
  }
1544
1586
 
1587
+ // Server KHÔNG gửi unread count tuyệt đối — chỉ push sequence mới.
1588
+ // Client tự compute: unread = max(newSequence - cached lastReadSequence, markedUnread ? 1 : 0)
1589
+ // Event chỉ gửi tới participant đang online, không gửi cho sender. System messages không bump sequence.
1545
1590
  interface UnreadCountChangedDto {
1546
1591
  conversationId: string;
1547
- unreadCount: number;
1592
+ newSequence: number; // MessageSequence mới sau khi bump
1593
+ senderId: string; // User vừa gửi tin (dùng để hiển thị preview)
1548
1594
  lastMessageAt: string; // ISO 8601
1549
1595
  lastMessageId: string;
1550
1596
  }
@@ -1592,11 +1638,13 @@ interface ConversationUnpinnedDto {
1592
1638
  conversationId: string;
1593
1639
  }
1594
1640
 
1641
+ // Chỉ gửi tới owner. Client cập nhật cached lastReadSequence rồi tính lại badge:
1642
+ // unread = max(cachedMessageSequence - lastReadSequence, markedUnread ? 1 : 0)
1595
1643
  interface ReadStatusChangedDto {
1596
1644
  conversationId: string;
1597
- lastReadMessageId?: string;
1598
- unreadCount: number;
1599
- markedAsUnread: boolean; // true khi user chủ động đánh dấu chưa đọc
1645
+ lastReadMessageId?: string | null; // null khi MarkAsUnread
1646
+ lastReadSequence: number; // High-water mark mới
1647
+ markedUnread: boolean; // true khi user chủ động đánh dấu chưa đọc
1600
1648
  }
1601
1649
 
1602
1650
  // Conversation bị ẩn/xóa khỏi danh sách của user.
@@ -2437,7 +2485,7 @@ const ms = Number(health.totalDuration); // NaN
2437
2485
  ### Ví dụ 1: Chat app cơ bản
2438
2486
 
2439
2487
  ```ts
2440
- import { ChatClient, ChatApiError } from '@manonero/chat-client-sdk';
2488
+ import { ChatClient, ChatApiError, computeUnreadCount } from '@manonero/chat-client-sdk';
2441
2489
  import type { ChatMessageDto } from '@manonero/chat-client-sdk';
2442
2490
 
2443
2491
  // 1. Khởi tạo
@@ -2456,8 +2504,13 @@ client.realtime.notifications.on('newMessageNotification', (notification) => {
2456
2504
  console.log(`[${notification.conversationId}] ${notification.senderName}: ${notification.contentPreview}`);
2457
2505
  });
2458
2506
 
2507
+ // Server chỉ push newSequence — client cập nhật cache rồi gọi helper để tính lại badge
2459
2508
  client.realtime.notifications.on('unreadCountChanged', (dto) => {
2460
- updateBadge(dto.conversationId, dto.unreadCount);
2509
+ const cached = getCachedConversation(dto.conversationId);
2510
+ cached.messageSequence = dto.newSequence;
2511
+ cached.lastMessageAt = dto.lastMessageAt;
2512
+ cached.lastMessageId = dto.lastMessageId;
2513
+ updateBadge(dto.conversationId, computeUnreadCount(cached));
2461
2514
  });
2462
2515
 
2463
2516
  // 5. Mở cuộc hội thoại và chat
@@ -48,11 +48,11 @@ export declare class ConversationApi {
48
48
  /**
49
49
  * POST /api/conversations/{id}/leave
50
50
  * Leave a Group conversation explicitly (Group only).
51
- * Returns 403 Forbidden if the conversation is not a Group.
51
+ * Returns 400 Bad Request if the conversation is not a Group.
52
52
  *
53
53
  * NOTE: `delete()` (DELETE) also performs the leave action for Group conversations and
54
54
  * additionally hides OneToOne/Self conversations. Use this method when you need the
55
- * strict Group-only semantic with explicit 403 feedback for non-Group conversations.
55
+ * strict Group-only semantic with explicit 400 feedback for non-Group conversations.
56
56
  */
57
57
  leaveGroup(id: string): Promise<void>;
58
58
  /**
@@ -66,11 +66,11 @@ export class ConversationApi {
66
66
  /**
67
67
  * POST /api/conversations/{id}/leave
68
68
  * Leave a Group conversation explicitly (Group only).
69
- * Returns 403 Forbidden if the conversation is not a Group.
69
+ * Returns 400 Bad Request if the conversation is not a Group.
70
70
  *
71
71
  * NOTE: `delete()` (DELETE) also performs the leave action for Group conversations and
72
72
  * additionally hides OneToOne/Self conversations. Use this method when you need the
73
- * strict Group-only semantic with explicit 403 feedback for non-Group conversations.
73
+ * strict Group-only semantic with explicit 400 feedback for non-Group conversations.
74
74
  */
75
75
  leaveGroup(id) {
76
76
  return this.http.post(`/api/conversations/${encodeURIComponent(id)}/leave`);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { CursorPaginatedResult, PagedResult, MediaReference, ProblemDetails, ProblemError, UploadProgressCallback, HealthStatus, HealthCheckResult, } from './types/common.js';
2
2
  export type { GoogleLoginRequest, DsAccountLoginRequest, DevLoginRequest, LoginRequest, AuthResponse, AuthUserInfo } from './types/auth.js';
3
3
  export type { ParticipantDto, CreateOrUpdateParticipantRequest } from './types/participant.js';
4
- export type { ConversationType, ConversationParticipantDto, ConversationDto, ConversationListItemDto, ConversationMediaKindFilter, ConversationMediaKind, ConversationMediaBlockType, ConversationMediaItemDto, CreateConversationRequest, UpdateConversationRequest, AddParticipantRequest, } from './types/conversation.js';
4
+ export type { ConversationType, ConversationParticipantDto, SelfReadState, ConversationDto, ConversationListItemDto, ConversationMediaKindFilter, ConversationMediaKind, ConversationMediaBlockType, ConversationMediaItemDto, CreateConversationRequest, UpdateConversationRequest, AddParticipantRequest, } from './types/conversation.js';
5
5
  export type { SenderType, SystemEventType, SystemEventInfo, ReactionGroup, ReactionSummary, MessageDto, SendMessageRequest, EditMessageRequest, MarkAsReadRequest, } from './types/message.js';
6
6
  export type { TextFormat, ButtonAction, ButtonStyle, ChoiceMode, QuickReplyAction, MentionType, TextBlock, ImageBlock, VideoBlock, AudioBlock, FileBlock, LinkPreviewBlock, EmbedBlock, LocationBlock, ContactBlock, ChoiceOption, ChoiceBlock, CardField, ActionButton, CardBlock, CarouselBlock, DividerBlock, CustomBlock, Block, QuickReply, Mention, } from './types/block.js';
7
7
  export { isTextBlock, isImageBlock, isVideoBlock, isAudioBlock, isFileBlock, isLinkPreviewBlock, isEmbedBlock, isLocationBlock, isContactBlock, isChoiceBlock, isCardBlock, isCarouselBlock, isDividerBlock, isCustomBlock, withTypeFirst, normalizeBlocksForSend, } from './types/block.js';
@@ -15,6 +15,8 @@ export { HttpClient } from './http/HttpClient.js';
15
15
  export type { HttpClientOptions } from './http/HttpClient.js';
16
16
  export { TypedEventEmitter } from './utils/TypedEventEmitter.js';
17
17
  export type { Unsubscribe } from './utils/TypedEventEmitter.js';
18
+ export { computeUnreadCount } from './utils/unread.js';
19
+ export type { UnreadCountFields } from './utils/unread.js';
18
20
  export { ChatHubClient } from './realtime/ChatHubClient.js';
19
21
  export type { ChatHubClientOptions, ChatHubEventMap } from './realtime/ChatHubClient.js';
20
22
  export { NotificationHubClient } from './realtime/NotificationHubClient.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,qBAAqB,EACrB,WAAW,EACX,cAAc,EACd,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,YAAY,EACZ,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG5I,YAAY,EAAE,cAAc,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAG/F,YAAY,EACV,gBAAgB,EAChB,0BAA0B,EAC1B,eAAe,EACf,uBAAuB,EACvB,2BAA2B,EAC3B,qBAAqB,EACrB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EACV,UAAU,EACV,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,SAAS,EACT,YAAY,EACZ,SAAS,EACT,aAAa,EACb,YAAY,EACZ,WAAW,EACX,KAAK,EACL,UAAU,EACV,OAAO,GACR,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGvG,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGjF,YAAY,EACV,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,yBAAyB,EACzB,gBAAgB,EAChB,sBAAsB,EACtB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAGhC,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EACV,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,EACtB,yBAAyB,EACzB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,YAAY,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAGhE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,YAAY,EACV,4BAA4B,EAC5B,uBAAuB,GACxB,MAAM,qCAAqC,CAAC;AAG7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,YAAY,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AAGpF,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ export { ChatApiError } from './errors/ChatApiError.js';
7
7
  export { HttpClient } from './http/HttpClient.js';
8
8
  // Utils — TypedEventEmitter
9
9
  export { TypedEventEmitter } from './utils/TypedEventEmitter.js';
10
+ // Utils — unread badge helper
11
+ export { computeUnreadCount } from './utils/unread.js';
10
12
  // Real-time — ChatHubClient
11
13
  export { ChatHubClient } from './realtime/ChatHubClient.js';
12
14
  // Real-time — NotificationHubClient
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AA8EpD,4CAA4C;AAC5C,OAAO,EACL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAgE1B,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,cAAc;AACd,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGlD,4BAA4B;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGjE,4BAA4B;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,oCAAoC;AACpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAM5E,kCAAkC;AAClC,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAGxE,yCAAyC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AA+EpD,4CAA4C;AAC5C,OAAO,EACL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,aAAa,EACb,WAAW,EACX,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAgE1B,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,cAAc;AACd,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGlD,4BAA4B;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGjE,8BAA8B;AAC9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGvD,4BAA4B;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,oCAAoC;AACpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAM5E,kCAAkC;AAClC,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAGxE,yCAAyC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
@@ -15,7 +15,20 @@ export interface ConversationParticipantDto {
15
15
  lastReadMessageId?: string | null;
16
16
  /** ISO 8601 */
17
17
  lastReadAt?: string | null;
18
- unreadCount: number;
18
+ }
19
+ /**
20
+ * Caller's per-conversation read state. Included on `ConversationDto`.
21
+ *
22
+ * `null` only when the caller is not an active participant; otherwise it
23
+ * carries the caller's own read pointer and mark-unread flag (other
24
+ * participants' values are private and not exposed by the server).
25
+ *
26
+ * Unread badge for this caller:
27
+ * `max(conversation.messageSequence - lastReadSequence, markedUnread ? 1 : 0)`
28
+ */
29
+ export interface SelfReadState {
30
+ lastReadSequence: number;
31
+ markedUnread: boolean;
19
32
  }
20
33
  /** Full conversation object */
21
34
  export interface ConversationDto {
@@ -28,13 +41,33 @@ export interface ConversationDto {
28
41
  /** ISO 8601, null when no messages yet */
29
42
  lastMessageAt?: string | null;
30
43
  lastMessageId?: string | null;
44
+ /**
45
+ * Monotonically increasing sequence assigned to the latest message in the
46
+ * conversation. Used together with `selfReadState` to compute the unread
47
+ * badge locally without a server-side precomputed count.
48
+ */
49
+ messageSequence: number;
31
50
  /** ISO 8601 */
32
51
  createdAt: string;
33
52
  /** ISO 8601 */
34
53
  updatedAt: string;
54
+ /** Caller's own read state. `null` when the caller is not an active participant. */
55
+ selfReadState: SelfReadState | null;
35
56
  participants: ConversationParticipantDto[];
36
57
  }
37
- /** Lightweight conversation item for listing */
58
+ /**
59
+ * Lightweight conversation item for listing.
60
+ *
61
+ * The server does NOT send a precomputed `unreadCount`. Instead it sends
62
+ * three fields that let clients compute the badge locally and keep it in
63
+ * sync with `UnreadCountChanged` / `ReadStatusChanged` SignalR events
64
+ * without refetching:
65
+ *
66
+ * unread = max(messageSequence - lastReadSequence, markedUnread ? 1 : 0)
67
+ *
68
+ * `lastReadSequence` and `markedUnread` are the **caller's** values — the
69
+ * list is already filtered to conversations the caller participates in.
70
+ */
38
71
  export interface ConversationListItemDto {
39
72
  id: string;
40
73
  type: ConversationType;
@@ -45,12 +78,17 @@ export interface ConversationListItemDto {
45
78
  lastMessageAt?: string | null;
46
79
  /** null when conversation has no messages yet */
47
80
  lastMessageId?: string | null;
81
+ /** Latest sequence number assigned to a message in this conversation. */
82
+ messageSequence: number;
83
+ /** Caller's last-read sequence (0 if never read). */
84
+ lastReadSequence: number;
85
+ /** Caller's mark-as-unread flag. */
86
+ markedUnread: boolean;
48
87
  /** Full message DTO for preview. null when conversation has no messages yet */
49
88
  lastMessage?: MessageDto | null;
50
89
  isPinned: boolean;
51
90
  /** ISO 8601, null when not pinned */
52
91
  pinnedAt?: string | null;
53
- unreadCount: number;
54
92
  participantCount: number;
55
93
  }
56
94
  export interface CreateConversationRequest {
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/types/conversation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/types/conversation.ts"],"names":[],"mappings":"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"}
@@ -39,9 +39,20 @@ export interface MentionedNotificationDto {
39
39
  /** ISO 8601 */
40
40
  sentAt: string;
41
41
  }
42
+ /**
43
+ * Server không gửi unread count tuyệt đối — chỉ push sequence mới khi
44
+ * `MessageSequence` của conversation tăng. Client tự compute badge:
45
+ * `unread = max(newSequence - cached lastReadSequence, markedUnread ? 1 : 0)`
46
+ *
47
+ * Event chỉ gửi tới participant đang online và KHÔNG gửi cho sender.
48
+ * System messages không bump sequence nên không phát sinh event này.
49
+ */
42
50
  export interface UnreadCountChangedDto {
43
51
  conversationId: string;
44
- unreadCount: number;
52
+ /** `MessageSequence` mới sau khi bump */
53
+ newSequence: number;
54
+ /** User vừa gửi tin (client có thể dùng để hiển thị preview) */
55
+ senderId: string;
45
56
  /** ISO 8601 */
46
57
  lastMessageAt: string;
47
58
  lastMessageId: string;
@@ -90,12 +101,20 @@ export interface ConversationPinnedDto {
90
101
  export interface ConversationUnpinnedDto {
91
102
  conversationId: string;
92
103
  }
104
+ /**
105
+ * Trạng thái đọc của conversation thay đổi do chính user này gọi
106
+ * `POST /api/conversations/{id}/read` hoặc `DELETE /api/conversations/{id}/read`.
107
+ * Chỉ gửi tới owner. Client cập nhật cached `lastReadSequence` rồi tính lại badge:
108
+ * `unread = max(cachedMessageSequence - lastReadSequence, markedUnread ? 1 : 0)`
109
+ */
93
110
  export interface ReadStatusChangedDto {
94
111
  conversationId: string;
95
- lastReadMessageId?: string;
96
- unreadCount: number;
97
- /** true if user explicitly marked as unread */
98
- markedAsUnread: boolean;
112
+ /** ID tin nhắn cuối đã đọc (null khi MarkAsUnread) */
113
+ lastReadMessageId?: string | null;
114
+ /** High-water mark mới (`Conversation.MessageSequence` tại thời điểm read) */
115
+ lastReadSequence: number;
116
+ /** `true` khi user thủ công Mark as Unread; `false` sau khi đọc thành công */
117
+ markedUnread: boolean;
99
118
  }
100
119
  /**
101
120
  * Conversation bị ẩn/xóa khỏi danh sách của user thực hiện hành động.
@@ -1 +1 @@
1
- {"version":3,"file":"notification-events.d.ts","sourceRoot":"","sources":["../../src/types/notification-events.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,yFAAyF;AACzF,MAAM,WAAW,yBAAyB;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,+EAA+E;AAC/E,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,oEAAoE;AACpE,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C,eAAe;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,yCAAyC;AACzC,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C,eAAe;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qEAAqE;AACrE,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe;IACf,UAAU,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"notification-events.d.ts","sourceRoot":"","sources":["../../src/types/notification-events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { ConversationDto, ConversationListItemDto } from '../types/conversation.js';
2
+ /**
3
+ * Minimal shape needed to compute an unread badge. `ConversationListItemDto`
4
+ * is structurally assignable to this; callers with their own cache shape can
5
+ * pass any object carrying the three fields.
6
+ */
7
+ export interface UnreadCountFields {
8
+ messageSequence: number;
9
+ lastReadSequence: number;
10
+ markedUnread: boolean;
11
+ }
12
+ /**
13
+ * Compute the caller's unread badge for a conversation.
14
+ *
15
+ * The server intentionally does not send a precomputed `unreadCount`; it sends
16
+ * the latest `messageSequence` plus the caller's `lastReadSequence` and
17
+ * `markedUnread` flag so clients can derive the badge locally and keep it in
18
+ * sync with `UnreadCountChanged` / `ReadStatusChanged` SignalR events without
19
+ * refetching. The formula is:
20
+ *
21
+ * max(messageSequence - lastReadSequence, markedUnread ? 1 : 0)
22
+ *
23
+ * The `max` with `0` (when `markedUnread` is false) also clamps the result if
24
+ * a transient race makes `lastReadSequence > messageSequence`. Returns `0`
25
+ * when given a `ConversationDto` whose `selfReadState` is `null` (caller is
26
+ * not an active participant).
27
+ */
28
+ export declare function computeUnreadCount(conversation: ConversationDto): number;
29
+ export declare function computeUnreadCount(item: ConversationListItemDto): number;
30
+ export declare function computeUnreadCount(fields: UnreadCountFields): number;
31
+ //# sourceMappingURL=unread.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unread.d.ts","sourceRoot":"","sources":["../../src/utils/unread.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEzF;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,eAAe,GAAG,MAAM,CAAC;AAC1E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,uBAAuB,GAAG,MAAM,CAAC;AAC1E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAAC"}
@@ -0,0 +1,10 @@
1
+ // utils/unread.ts — Unread count helper
2
+ export function computeUnreadCount(input) {
3
+ if ('selfReadState' in input) {
4
+ if (input.selfReadState === null)
5
+ return 0;
6
+ return Math.max(input.messageSequence - input.selfReadState.lastReadSequence, input.selfReadState.markedUnread ? 1 : 0);
7
+ }
8
+ return Math.max(input.messageSequence - input.lastReadSequence, input.markedUnread ? 1 : 0);
9
+ }
10
+ //# sourceMappingURL=unread.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unread.js","sourceRoot":"","sources":["../../src/utils/unread.ts"],"names":[],"mappings":"AAAA,wCAAwC;AAkCxC,MAAM,UAAU,kBAAkB,CAChC,KAA0C;IAE1C,IAAI,eAAe,IAAI,KAAK,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,aAAa,KAAK,IAAI;YAAE,OAAO,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,GAAG,CACb,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,gBAAgB,EAC5D,KAAK,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACzC,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CACb,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC,gBAAgB,EAC9C,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC3B,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manonero/chat-client-sdk",
3
- "version": "1.0.0-beta.11",
3
+ "version": "1.0.0-beta.12",
4
4
  "description": "A TypeScript SDK for building chat clients using Microsoft SignalR.",
5
5
  "author": "Manonero",
6
6
  "license": "MIT",