stream-chat 9.21.0 → 9.22.1

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.
@@ -1,8 +1,9 @@
1
1
  import { ChannelState } from './channel_state';
2
+ import { MessageComposer } from './messageComposer';
3
+ import { MessageReceiptsTracker } from './messageDelivery';
2
4
  import type { StreamChat } from './client';
3
5
  import type { AIState, APIResponse, AscDesc, BanUserOptions, ChannelAPIResponse, ChannelData, ChannelMemberAPIResponse, ChannelMemberResponse, ChannelQueryOptions, ChannelResponse, ChannelUpdateOptions, CreateDraftResponse, DeleteChannelAPIResponse, DraftMessagePayload, Event, EventAPIResponse, EventHandler, EventTypes, GetDraftResponse, GetMultipleMessagesAPIResponse, GetReactionsAPIResponse, GetRepliesAPIResponse, LiveLocationPayload, LocalMessage, MarkReadOptions, MarkUnreadOptions, MemberFilters, MemberSort, Message, MessageFilters, MessageOptions, MessagePaginationOptions, MessageResponse, MessageSetType, MuteChannelAPIResponse, NewMemberPayload, PartialUpdateChannel, PartialUpdateChannelAPIResponse, PartialUpdateMember, PartialUpdateMemberAPIResponse, PinnedMessagePaginationOptions, PinnedMessagesSort, PollVoteData, PushPreference, QueryChannelAPIResponse, QueryMembersOptions, Reaction, ReactionAPIResponse, SearchAPIResponse, SearchOptions, SendMessageAPIResponse, SendMessageOptions, SendReactionOptions, StaticLocationPayload, TruncateChannelAPIResponse, TruncateOptions, UpdateChannelAPIResponse, UpdateChannelOptions, UpdateLocationPayload, UserResponse } from './types';
4
6
  import type { Role } from './permissions';
5
- import { MessageComposer } from './messageComposer';
6
7
  /**
7
8
  * Channel - The Channel class manages it's own state.
8
9
  */
@@ -39,6 +40,7 @@ export declare class Channel {
39
40
  disconnected: boolean;
40
41
  push_preferences?: PushPreference;
41
42
  readonly messageComposer: MessageComposer;
43
+ readonly messageReceiptsTracker: MessageReceiptsTracker;
42
44
  /**
43
45
  * constructor - Create a channel
44
46
  *
@@ -440,12 +442,19 @@ export declare class Channel {
440
442
  */
441
443
  lastMessage(): LocalMessage | undefined;
442
444
  /**
443
- * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled
445
+ * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled. Syncs the message delivery report candidates local state.
444
446
  *
445
447
  * @param {MarkReadOptions} data
446
448
  * @return {Promise<EventAPIResponse | null>} Description
447
449
  */
448
450
  markRead(data?: MarkReadOptions): Promise<EventAPIResponse | null>;
451
+ /**
452
+ * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is enabled
453
+ *
454
+ * @param {MarkReadOptions} data
455
+ * @return {Promise<EventAPIResponse | null>} Description
456
+ */
457
+ markAsReadRequest(data?: MarkReadOptions): Promise<EventAPIResponse | null>;
449
458
  /**
450
459
  * markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is enabled
451
460
  *
@@ -206,9 +206,15 @@ export declare class ChannelState {
206
206
  * @return {ReturnType<ChannelState['formatMessage']>} Returns the message, or undefined if the message wasn't found
207
207
  */
208
208
  findMessage(messageId: string, parentMessageId?: string): LocalMessage | undefined;
209
+ findMessageByTimestamp(timestampMs: number, parentMessageId?: string, exactTsMatch?: boolean): LocalMessage | null;
209
210
  private switchToMessageSet;
210
211
  private areMessageSetsOverlap;
211
212
  private findMessageSetIndex;
213
+ /**
214
+ * Identifies the set index into which a message set would pertain if its first item's creation date corresponded to oldestTimestampMs.
215
+ * @param oldestTimestampMs
216
+ */
217
+ private findMessageSetByOldestTimestamp;
212
218
  private findTargetMessageSet;
213
219
  }
214
220
  export {};
@@ -7,7 +7,7 @@ import { TokenManager } from './token_manager';
7
7
  import { WSConnectionFallback } from './connection_fallback';
8
8
  import { Campaign } from './campaign';
9
9
  import { Segment } from './segment';
10
- import type { ActiveLiveLocationsAPIResponse, APIErrorResponse, APIResponse, AppSettings, AppSettingsAPIResponse, BannedUsersFilters, BannedUsersPaginationOptions, BannedUsersResponse, BannedUsersSort, BanUserOptions, BaseDeviceFields, BlockList, BlockListResponse, BlockUserAPIResponse, CampaignData, CampaignFilters, CampaignQueryOptions, CampaignResponse, CampaignSort, CastVoteAPIResponse, ChannelAPIResponse, ChannelData, ChannelFilters, ChannelMute, ChannelOptions, ChannelResponse, ChannelSort, ChannelStateOptions, CheckPushResponse, CheckSNSResponse, CheckSQSResponse, Configs, ConnectAPIResponse, CreateChannelOptions, CreateChannelResponse, CreateCommandOptions, CreateCommandResponse, CreateImportOptions, CreateImportResponse, CreateImportURLResponse, CreatePollAPIResponse, CreatePollData, CreatePollOptionAPIResponse, CreateReminderOptions, CustomPermissionOptions, DeactivateUsersOptions, DeleteCommandResponse, DeleteUserOptions, Device, DeviceIdentifier, DraftFilters, DraftSort, EndpointName, Event, EventAPIResponse, EventHandler, ExportChannelOptions, ExportChannelRequest, ExportChannelResponse, ExportChannelStatusResponse, ExportUsersRequest, ExportUsersResponse, FlagMessageResponse, FlagReportsFilters, FlagReportsPaginationOptions, FlagReportsResponse, FlagsFilters, FlagsPaginationOptions, FlagsResponse, FlagUserResponse, GetBlockedUsersAPIResponse, GetCampaignOptions, GetChannelTypeResponse, GetCommandResponse, GetHookEventsResponse, GetImportResponse, GetMessageOptions, GetPollAPIResponse, GetRateLimitsResponse, GetThreadAPIResponse, GetThreadOptions, GetUnreadCountAPIResponse, GetUnreadCountBatchAPIResponse, ListChannelResponse, ListCommandsResponse, ListImportsPaginationOptions, ListImportsResponse, LocalMessage, Logger, MarkChannelsReadOptions, MarkDeliveredOptions, MessageFilters, MessageFlagsFilters, MessageFlagsPaginationOptions, MessageFlagsResponse, MessageResponse, Mute, MuteUserOptions, MuteUserResponse, OGAttachment, OwnUserResponse, Pager, PartialMessageUpdate, PartialPollUpdate, PartialThreadUpdate, PartialUserUpdate, PermissionAPIResponse, PermissionsAPIResponse, PollAnswersAPIResponse, PollData, PollOptionData, PollSort, PollVote, PollVoteData, PollVotesAPIResponse, Product, PushPreference, PushProvider, PushProviderConfig, PushProviderID, PushProviderListResponse, PushProviderUpsertResponse, QueryDraftsResponse, QueryMessageHistoryFilters, QueryMessageHistoryOptions, QueryMessageHistoryResponse, QueryMessageHistorySort, QueryPollsFilters, QueryPollsOptions, QueryPollsResponse, QueryReactionsAPIResponse, QueryReactionsOptions, QueryRemindersOptions, QueryRemindersResponse, QuerySegmentsOptions, QuerySegmentTargetsFilter, QueryThreadsOptions, QueryVotesFilters, QueryVotesOptions, ReactionFilters, ReactionResponse, ReactionSort, ReactivateUserOptions, ReactivateUsersOptions, ReminderAPIResponse, ReviewFlagReportOptions, ReviewFlagReportResponse, SdkIdentifier, SearchAPIResponse, SearchOptions, SegmentData, SegmentResponse, SegmentTargetsResponse, SegmentType, SendFileAPIResponse, SharedLocationResponse, SortParam, StreamChatOptions, SyncOptions, SyncResponse, TaskResponse, TaskStatus, TestPushDataInput, TestSNSDataInput, TestSQSDataInput, TokenOrProvider, TranslateResponse, UnBanUserOptions, UpdateChannelTypeRequest, UpdateChannelTypeResponse, UpdateCommandOptions, UpdateCommandResponse, UpdateLocationPayload, UpdateMessageAPIResponse, UpdateMessageOptions, UpdatePollAPIResponse, UpdateReminderOptions, UpdateSegmentData, UpsertPushPreferencesResponse, UserCustomEvent, UserFilters, UserOptions, UserResponse, UserSort, VoteSort } from './types';
10
+ import type { ActiveLiveLocationsAPIResponse, APIErrorResponse, APIResponse, AppSettings, AppSettingsAPIResponse, BannedUsersFilters, BannedUsersPaginationOptions, BannedUsersResponse, BannedUsersSort, BanUserOptions, BaseDeviceFields, BlockList, BlockListResponse, BlockUserAPIResponse, CampaignData, CampaignFilters, CampaignQueryOptions, CampaignResponse, CampaignSort, CastVoteAPIResponse, ChannelAPIResponse, ChannelData, ChannelFilters, ChannelMute, ChannelOptions, ChannelResponse, ChannelSort, ChannelStateOptions, CheckPushResponse, CheckSNSResponse, CheckSQSResponse, Configs, ConnectAPIResponse, CreateChannelOptions, CreateChannelResponse, CreateCommandOptions, CreateCommandResponse, CreateImportOptions, CreateImportResponse, CreateImportURLResponse, CreatePollAPIResponse, CreatePollData, CreatePollOptionAPIResponse, CreateReminderOptions, CustomPermissionOptions, DeactivateUsersOptions, DeleteCommandResponse, DeleteMessageOptions, DeleteUserOptions, Device, DeviceIdentifier, DraftFilters, DraftSort, EndpointName, Event, EventAPIResponse, EventHandler, ExportChannelOptions, ExportChannelRequest, ExportChannelResponse, ExportChannelStatusResponse, ExportUsersRequest, ExportUsersResponse, FlagMessageResponse, FlagReportsFilters, FlagReportsPaginationOptions, FlagReportsResponse, FlagsFilters, FlagsPaginationOptions, FlagsResponse, FlagUserResponse, GetBlockedUsersAPIResponse, GetCampaignOptions, GetChannelTypeResponse, GetCommandResponse, GetHookEventsResponse, GetImportResponse, GetMessageOptions, GetPollAPIResponse, GetRateLimitsResponse, GetThreadAPIResponse, GetThreadOptions, GetUnreadCountAPIResponse, GetUnreadCountBatchAPIResponse, ListChannelResponse, ListCommandsResponse, ListImportsPaginationOptions, ListImportsResponse, LocalMessage, Logger, MarkChannelsReadOptions, MarkDeliveredOptions, MessageFilters, MessageFlagsFilters, MessageFlagsPaginationOptions, MessageFlagsResponse, MessageResponse, Mute, MuteUserOptions, MuteUserResponse, OGAttachment, OwnUserResponse, Pager, PartialMessageUpdate, PartialPollUpdate, PartialThreadUpdate, PartialUserUpdate, PermissionAPIResponse, PermissionsAPIResponse, PollAnswersAPIResponse, PollData, PollOptionData, PollSort, PollVote, PollVoteData, PollVotesAPIResponse, Product, PushPreference, PushProvider, PushProviderConfig, PushProviderID, PushProviderListResponse, PushProviderUpsertResponse, QueryDraftsResponse, QueryMessageHistoryFilters, QueryMessageHistoryOptions, QueryMessageHistoryResponse, QueryMessageHistorySort, QueryPollsFilters, QueryPollsOptions, QueryPollsResponse, QueryReactionsAPIResponse, QueryReactionsOptions, QueryRemindersOptions, QueryRemindersResponse, QuerySegmentsOptions, QuerySegmentTargetsFilter, QueryThreadsOptions, QueryVotesFilters, QueryVotesOptions, ReactionFilters, ReactionResponse, ReactionSort, ReactivateUserOptions, ReactivateUsersOptions, ReminderAPIResponse, ReviewFlagReportOptions, ReviewFlagReportResponse, SdkIdentifier, SearchAPIResponse, SearchOptions, SegmentData, SegmentResponse, SegmentTargetsResponse, SegmentType, SendFileAPIResponse, SharedLocationResponse, SortParam, StreamChatOptions, SyncOptions, SyncResponse, TaskResponse, TaskStatus, TestPushDataInput, TestSNSDataInput, TestSQSDataInput, TokenOrProvider, TranslateResponse, UnBanUserOptions, UpdateChannelTypeRequest, UpdateChannelTypeResponse, UpdateCommandOptions, UpdateCommandResponse, UpdateLocationPayload, UpdateMessageAPIResponse, UpdateMessageOptions, UpdatePollAPIResponse, UpdateReminderOptions, UpdateSegmentData, UpsertPushPreferencesResponse, UserCustomEvent, UserFilters, UserOptions, UserResponse, UserSort, VoteSort } from './types';
11
11
  import { ErrorFromResponse } from './types';
12
12
  import { InsightMetrics } from './insights';
13
13
  import { Thread } from './thread';
@@ -16,6 +16,7 @@ import { ThreadManager } from './thread_manager';
16
16
  import { PollManager } from './poll_manager';
17
17
  import type { ChannelManagerEventHandlerOverrides, ChannelManagerOptions, QueryChannelsRequestType } from './channel_manager';
18
18
  import { ChannelManager } from './channel_manager';
19
+ import { MessageDeliveryReporter } from './messageDelivery';
19
20
  import { NotificationManager } from './notifications';
20
21
  import { ReminderManager } from './reminders';
21
22
  import { StateStore } from './store';
@@ -38,6 +39,7 @@ export type MessageComposerSetupState = {
38
39
  };
39
40
  export declare class StreamChat {
40
41
  private static _instance?;
42
+ messageDeliveryReporter: MessageDeliveryReporter;
41
43
  _user?: OwnUserResponse | UserResponse;
42
44
  appSettingsPromise?: Promise<AppSettingsAPIResponse>;
43
45
  activeChannels: {
@@ -1064,6 +1066,7 @@ export declare class StreamChat {
1064
1066
  status?: string;
1065
1067
  thread_participants?: UserResponse[];
1066
1068
  updated_at?: string;
1069
+ deleted_for_me?: boolean;
1067
1070
  } & {
1068
1071
  quoted_message?: import("./types").MessageResponseBase;
1069
1072
  }>;
@@ -1140,10 +1143,17 @@ export declare class StreamChat {
1140
1143
  partialUpdateMessage(id: string, partialMessageObject: PartialMessageUpdate, partialUserOrUserId?: string | {
1141
1144
  id: string;
1142
1145
  }, options?: UpdateMessageOptions): Promise<UpdateMessageAPIResponse>;
1143
- deleteMessage(messageID: string, hardDelete?: boolean): Promise<APIResponse & {
1146
+ /**
1147
+ * deleteMessage - Delete a message
1148
+ *
1149
+ * @param {string} messageID The id of the message to delete
1150
+ * @param {boolean | DeleteMessageOptions | undefined} [optionsOrHardDelete]
1151
+ * @return {Promise<APIResponse & { message: MessageResponse }>} The API response
1152
+ */
1153
+ deleteMessage(messageID: string, optionsOrHardDelete?: DeleteMessageOptions | boolean): Promise<APIResponse & {
1144
1154
  message: MessageResponse;
1145
1155
  }>;
1146
- _deleteMessage(messageID: string, hardDelete?: boolean): Promise<APIResponse & {
1156
+ _deleteMessage(messageID: string, optionsOrHardDelete?: DeleteMessageOptions | boolean): Promise<APIResponse & {
1147
1157
  message: MessageResponse;
1148
1158
  }>;
1149
1159
  /**
@@ -1730,6 +1740,7 @@ export declare class StreamChat {
1730
1740
  status?: string;
1731
1741
  thread_participants?: UserResponse[];
1732
1742
  updated_at?: string;
1743
+ deleted_for_me?: boolean;
1733
1744
  } & {
1734
1745
  quoted_message?: import("./types").MessageResponseBase;
1735
1746
  }>;
@@ -1971,11 +1982,12 @@ export declare class StreamChat {
1971
1982
  */
1972
1983
  deleteImage(url: string): Promise<APIResponse>;
1973
1984
  /**
1974
- * Send the mark delivered event for this user, only works if the `delivery_receipts` setting is enabled
1985
+ * Send the mark delivered event for this user
1975
1986
  *
1976
1987
  * @param {MarkDeliveredOptions} data
1977
1988
  * @return {Promise<EventAPIResponse | void>} Description
1978
1989
  */
1979
- markChannelsDelivered(data?: MarkDeliveredOptions): Promise<EventAPIResponse | undefined>;
1990
+ markChannelsDelivered(data: MarkDeliveredOptions): Promise<EventAPIResponse | undefined>;
1991
+ syncDeliveredCandidates(collections: Channel[]): void;
1980
1992
  }
1981
1993
  export {};
@@ -31,6 +31,7 @@ export declare const EVENT_MAP: {
31
31
  'notification.mark_unread': boolean;
32
32
  'notification.message_new': boolean;
33
33
  'notification.mutes_updated': boolean;
34
+ 'notification.reminder_due': boolean;
34
35
  'notification.removed_from_channel': boolean;
35
36
  'notification.thread_message_new': boolean;
36
37
  'poll.closed': boolean;
@@ -41,6 +42,9 @@ export declare const EVENT_MAP: {
41
42
  'reaction.deleted': boolean;
42
43
  'reaction.new': boolean;
43
44
  'reaction.updated': boolean;
45
+ 'reminder.created': boolean;
46
+ 'reminder.deleted': boolean;
47
+ 'reminder.updated': boolean;
44
48
  'thread.updated': boolean;
45
49
  'typing.start': boolean;
46
50
  'typing.stop': boolean;
@@ -64,8 +68,4 @@ export declare const EVENT_MAP: {
64
68
  'capabilities.changed': boolean;
65
69
  'live_location_sharing.started': boolean;
66
70
  'live_location_sharing.stopped': boolean;
67
- 'reminder.created': boolean;
68
- 'reminder.updated': boolean;
69
- 'reminder.deleted': boolean;
70
- 'notification.reminder_due': boolean;
71
71
  };
@@ -8,6 +8,7 @@ export * from './connection';
8
8
  export * from './events';
9
9
  export * from './insights';
10
10
  export * from './messageComposer';
11
+ export * from './messageDelivery';
11
12
  export * from './middleware';
12
13
  export * from './moderation';
13
14
  export * from './notifications';
@@ -0,0 +1,74 @@
1
+ import type { StreamChat } from '../client';
2
+ import { Channel } from '../channel';
3
+ import { Thread } from '../thread';
4
+ import type { EventAPIResponse, MarkDeliveredOptions, MarkReadOptions } from '../types';
5
+ type MessageId = string;
6
+ type ChannelThreadCompositeId = string;
7
+ export type AnnounceDeliveryOptions = Omit<MarkDeliveredOptions, 'latest_delivered_messages'>;
8
+ export type MessageDeliveryReporterOptions = {
9
+ client: StreamChat;
10
+ };
11
+ export declare class MessageDeliveryReporter {
12
+ protected client: StreamChat;
13
+ protected deliveryReportCandidates: Map<ChannelThreadCompositeId, MessageId>;
14
+ protected nextDeliveryReportCandidates: Map<ChannelThreadCompositeId, MessageId>;
15
+ protected markDeliveredRequestPromise: Promise<EventAPIResponse | void> | null;
16
+ protected markDeliveredTimeout: ReturnType<typeof setTimeout> | null;
17
+ constructor({ client }: MessageDeliveryReporterOptions);
18
+ private get markDeliveredRequestInFlight();
19
+ private get hasTimer();
20
+ private get hasDeliveryCandidates();
21
+ /**
22
+ * Build latest_delivered_messages payload from an arbitrary buffer (deliveryReportCandidates / nextDeliveryReportCandidates)
23
+ */
24
+ private confirmationsFrom;
25
+ private confirmationsFromDeliveryReportCandidates;
26
+ /**
27
+ * Generate candidate key for storing in the candidates buffer
28
+ * @param collection
29
+ * @private
30
+ */
31
+ private candidateKeyFor;
32
+ /**
33
+ * Retrieve the reference to the latest message in the state that is nor read neither reported as delivered
34
+ * @param collection
35
+ */
36
+ private getNextDeliveryReportCandidate;
37
+ /**
38
+ * Updates the delivery candidates buffer with the latest delivery candidates
39
+ * @param collection
40
+ */
41
+ private trackDeliveredCandidate;
42
+ /**
43
+ * Removes candidate from the delivery report buffer
44
+ * @param collection
45
+ * @private
46
+ */
47
+ private removeCandidateFor;
48
+ /**
49
+ * Records the latest message delivered for Channel or Thread instances and schedules the next report
50
+ * if not already scheduled and candidates exist.
51
+ * Should be used for WS handling (message.new) as well as for ingesting HTTP channel query results.
52
+ * @param collections
53
+ */
54
+ syncDeliveredCandidates(collections: (Channel | Thread)[]): void;
55
+ /**
56
+ * Fires delivery announcement request followed by immediate delivery candidate buffer reset.
57
+ * @param options
58
+ */
59
+ announceDelivery: (options?: AnnounceDeliveryOptions) => void;
60
+ announceDeliveryBuffered: (options?: AnnounceDeliveryOptions) => void;
61
+ /**
62
+ * Delegates the mark-read call to the Channel or Thread instance
63
+ * @param collection
64
+ * @param options
65
+ */
66
+ markRead: (collection: Channel | Thread, options?: MarkReadOptions) => Promise<EventAPIResponse | null>;
67
+ /**
68
+ * Throttles the MessageDeliveryReporter.markRead call
69
+ * @param collection
70
+ * @param options
71
+ */
72
+ throttledMarkRead: (collection: Channel | Thread, options?: MarkReadOptions | undefined) => void;
73
+ }
74
+ export {};
@@ -0,0 +1,121 @@
1
+ import type { ReadResponse, UserResponse } from '../types';
2
+ type MessageId = string;
3
+ export type MsgRef = {
4
+ timestampMs: number;
5
+ msgId: MessageId;
6
+ };
7
+ export type OwnMessageReceiptsTrackerMessageLocator = (timestampMs: number) => MsgRef | null;
8
+ export type UserProgress = {
9
+ user: UserResponse;
10
+ lastReadRef: MsgRef;
11
+ lastDeliveredRef: MsgRef;
12
+ };
13
+ export type OwnMessageReceiptsTrackerOptions = {
14
+ locateMessage: OwnMessageReceiptsTrackerMessageLocator;
15
+ };
16
+ /**
17
+ * MessageReceiptsTracker
18
+ * --------------------------------
19
+ * Tracks **other participants’** delivery/read progress toward **own (outgoing) messages**
20
+ * within a **single timeline** (one channel/thread).
21
+ *
22
+ * How it works
23
+ * ------------
24
+ * - Each user has a compact progress record:
25
+ * - `lastReadRef`: latest message they have **read**
26
+ * - `lastDeliveredRef`: latest message they have **received** (always `>= lastReadRef`)
27
+ * - Internally keeps two arrays sorted **ascending by timestamp**:
28
+ * - `readSorted` (by `lastReadRef`)
29
+ * - `deliveredSorted` (by `lastDeliveredRef`)
30
+ * - Queries like “who read message M?” become a **binary search + suffix slice**.
31
+ *
32
+ * Construction
33
+ * ------------
34
+ * `new MessageReceiptsTracker({locateMessage})`
35
+ * - `locateMessage(timestamp) => MsgRef | null` must resolve a message ref representation - `{ timestamp, msgId }`.
36
+ * - If `locateMessage` returns `null`, the event is ignored (message unknown locally).
37
+ *
38
+ * Event ingestion
39
+ * ---------------
40
+ * - `ingestInitial(rows: ReadResponse[])`: Builds initial state from server snapshot.
41
+ * If a user’s `last_read` is ahead of `last_delivered_at`, the tracker enforces
42
+ * the invariant `lastDeliveredRef >= lastReadRef`.
43
+ * - `onMessageRead(user, readAtISO)`:
44
+ * Advances the user’s read; also bumps delivered to match if needed.
45
+ * - `onMessageDelivered(user, deliveredAtISO)`:
46
+ * Advances the user’s delivered to `max(currentRead, deliveredAt)`.
47
+ *
48
+ * Queries
49
+ * -------
50
+ * - `readersForMessage(msgRef) : UserResponse[]` → users with `lastReadRef >= msgRef`
51
+ * - `deliveredForMessage(msgRef) : UserResponse[]` → users with `lastDeliveredRef >= msgRef`
52
+ * - `deliveredNotReadForMessage(msgRef): UserResponse[]` → delivered but `lastReadRef < msgRef`
53
+ * - `usersWhoseLastReadIs : UserResponse[]` → users for whom `msgRef` is their *last read* (exact match)
54
+ * - `usersWhoseLastDeliveredIs : UserResponse[]` → users for whom `msgRef` is their *last delivered* (exact match)
55
+ * - `groupUsersByLastReadMessage : Record<MsgId, UserResponse[]> → mapping of messages to their readers
56
+ * - `groupUsersByLastDeliveredMessage : Record<MsgId, UserResponse[]> → mapping of messages to their receivers
57
+ * - `hasUserRead(msgRef, userId) : boolean`
58
+ * - `hasUserDelivered(msgRef, userId) : boolean`
59
+ *
60
+ * Complexity
61
+ * ----------
62
+ * - Update on read/delivered: **O(log U)** (binary search + one splice) per event, where U is count of users stored by tracker.
63
+ * - Query lists: **O(log U + K)** where `K` is the number of returned users (suffix length).
64
+ * - Memory: **O(U)** - tracker’s memory grows linearly with the number of users in the channel/thread and does not depend on the number of messages.
65
+ *
66
+ * Scope & notes
67
+ * -------------
68
+ * - One tracker instance is **scoped to a single timeline**. Instantiate per channel/thread.
69
+ * - Ordering is by **ascending timestamp**; ties are kept stable by inserting at the end of the
70
+ * equal-timestamp plateau (upper-bound insertion), preserving intuitive arrival order.
71
+ * - This tracker models **others’ progress toward own messages**;
72
+ */
73
+ export declare class MessageReceiptsTracker {
74
+ private byUser;
75
+ private readSorted;
76
+ private deliveredSorted;
77
+ private locateMessage;
78
+ constructor({ locateMessage }: OwnMessageReceiptsTrackerOptions);
79
+ /** Build initial state from server snapshots (single pass + sort). */
80
+ ingestInitial(responses: ReadResponse[]): void;
81
+ /** message.delivered — user device confirmed delivery up to and including messageId. */
82
+ onMessageDelivered({ user, deliveredAt, lastDeliveredMessageId, }: {
83
+ user: UserResponse;
84
+ deliveredAt: string;
85
+ lastDeliveredMessageId?: string;
86
+ }): void;
87
+ /** message.read — user read up to and including messageId. */
88
+ onMessageRead({ user, readAt, lastReadMessageId, }: {
89
+ user: UserResponse;
90
+ readAt: string;
91
+ lastReadMessageId?: string;
92
+ }): void;
93
+ /** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`.
94
+ * Sets lastReadRef to the event’s last_read_* values. Delivery never moves backward.
95
+ * The event is sent only to the user that triggered the action (own user), so we will never adjust read ref
96
+ * for other users - we will not see changes in the UI for other users. However, this implementation does not
97
+ * take into consideration this fact and is ready to handle the mark-unread event for any user.
98
+ */
99
+ onNotificationMarkUnread({ user, lastReadAt, lastReadMessageId, }: {
100
+ user: UserResponse;
101
+ lastReadAt?: string;
102
+ lastReadMessageId?: string;
103
+ }): void;
104
+ /** All users who READ this message. */
105
+ readersForMessage(msgRef: MsgRef): UserResponse[];
106
+ /** All users who have it DELIVERED (includes readers). */
107
+ deliveredForMessage(msgRef: MsgRef): UserResponse[];
108
+ /** Users who delivered but have NOT read. */
109
+ deliveredNotReadForMessage(msgRef: MsgRef): UserResponse[];
110
+ /** Users for whom `msgRef` is their *last read* (exact match). */
111
+ usersWhoseLastReadIs(msgRef: MsgRef): UserResponse[];
112
+ /** Users for whom `msgRef` is their *last delivered* (exact match). */
113
+ usersWhoseLastDeliveredIs(msgRef: MsgRef): UserResponse[];
114
+ hasUserRead(msgRef: MsgRef, userId: string): boolean;
115
+ hasUserDelivered(msgRef: MsgRef, userId: string): boolean;
116
+ getUserProgress(userId: string): UserProgress | null;
117
+ groupUsersByLastReadMessage(): Record<MessageId, UserResponse[]>;
118
+ groupUsersByLastDeliveredMessage(): Record<MessageId, UserResponse[]>;
119
+ private ensureUser;
120
+ }
121
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './MessageDeliveryReporter';
2
+ export * from './MessageReceiptsTracker';
@@ -605,6 +605,7 @@ export type MessageResponseBase = MessageBase & {
605
605
  status?: string;
606
606
  thread_participants?: UserResponse[];
607
607
  updated_at?: string;
608
+ deleted_for_me?: boolean;
608
609
  };
609
610
  export type ReactionGroupResponse = {
610
611
  count: number;
@@ -821,7 +822,7 @@ export type BanUserOptions = UnBanUserOptions & {
821
822
  ip_ban?: boolean;
822
823
  reason?: string;
823
824
  timeout?: number;
824
- delete_messages?: DeleteMessagesOptions;
825
+ delete_messages?: MessageDeletionStrategy;
825
826
  };
826
827
  export type ChannelOptions = {
827
828
  limit?: number;
@@ -1271,13 +1272,14 @@ export type Event = CustomEventData & {
1271
1272
  ai_state?: AIState;
1272
1273
  channel?: ChannelResponse;
1273
1274
  channel_custom?: CustomChannelData;
1274
- channel_member_count?: number;
1275
1275
  channel_id?: string;
1276
+ channel_member_count?: number;
1276
1277
  channel_type?: string;
1277
1278
  cid?: string;
1278
1279
  clear_history?: boolean;
1279
1280
  connection_id?: string;
1280
1281
  created_at?: string;
1282
+ deleted_for_me?: boolean;
1281
1283
  draft?: DraftResponse;
1282
1284
  first_unread_message_id?: string;
1283
1285
  hard_delete?: boolean;
@@ -2627,13 +2629,18 @@ export type CustomCheckFlag = {
2627
2629
  labels?: string[];
2628
2630
  reason?: string;
2629
2631
  };
2630
- export type DeleteMessagesOptions = 'soft' | 'hard';
2632
+ export type MessageDeletionStrategy = 'soft' | 'hard';
2633
+ export type DeleteMessagesOptions = MessageDeletionStrategy;
2634
+ export type DeleteMessageOptions = {
2635
+ deleteForMe?: boolean;
2636
+ hardDelete?: boolean;
2637
+ };
2631
2638
  export type SubmitActionOptions = {
2632
2639
  ban?: {
2633
2640
  channel_ban_only?: boolean;
2634
2641
  reason?: string;
2635
2642
  timeout?: number;
2636
- delete_messages?: DeleteMessagesOptions;
2643
+ delete_messages?: MessageDeletionStrategy;
2637
2644
  };
2638
2645
  delete_message?: {
2639
2646
  hard_delete?: boolean;
@@ -136,6 +136,7 @@ export declare const toDeletedMessage: ({ message, deletedAt, hardDelete, }: {
136
136
  shadowed?: boolean | undefined;
137
137
  shared_location?: import("./types").SharedLocationResponse | undefined;
138
138
  thread_participants?: UserResponse[] | undefined;
139
+ deleted_for_me?: boolean | undefined;
139
140
  created_at: Date;
140
141
  pinned_at: Date | null;
141
142
  status: string;
@@ -212,7 +213,7 @@ export declare const debounce: <T extends (...args: any[]) => any>(fn: T, timeou
212
213
  leading?: boolean;
213
214
  trailing?: boolean;
214
215
  }) => DebouncedFunc<T>;
215
- export declare const throttle: <T extends (...args: unknown[]) => unknown>(fn: T, timeout?: number, { leading, trailing }?: {
216
+ export declare const throttle: <T extends (...args: any[]) => any>(fn: T, timeout?: number, { leading, trailing }?: {
216
217
  leading?: boolean;
217
218
  trailing?: boolean;
218
219
  }) => (...args: Parameters<T>) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.21.0",
3
+ "version": "9.22.1",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
package/src/channel.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ChannelState } from './channel_state';
2
+ import { MessageComposer } from './messageComposer';
3
+ import { MessageReceiptsTracker } from './messageDelivery';
2
4
  import {
3
5
  generateChannelTempCid,
4
6
  logChatPromiseExecution,
@@ -74,7 +76,6 @@ import type {
74
76
  } from './types';
75
77
  import type { Role } from './permissions';
76
78
  import type { CustomChannelData } from './custom_types';
77
- import { MessageComposer } from './messageComposer';
78
79
 
79
80
  /**
80
81
  * Channel - The Channel class manages it's own state.
@@ -110,6 +111,7 @@ export class Channel {
110
111
  disconnected: boolean;
111
112
  push_preferences?: PushPreference;
112
113
  public readonly messageComposer: MessageComposer;
114
+ public readonly messageReceiptsTracker: MessageReceiptsTracker;
113
115
 
114
116
  /**
115
117
  * constructor - Create a channel
@@ -158,6 +160,13 @@ export class Channel {
158
160
  client: this._client,
159
161
  compositionContext: this,
160
162
  });
163
+
164
+ this.messageReceiptsTracker = new MessageReceiptsTracker({
165
+ locateMessage: (timestampMs) => {
166
+ const msg = this.state.findMessageByTimestamp(timestampMs);
167
+ return msg && { timestampMs, msgId: msg.id };
168
+ },
169
+ });
161
170
  }
162
171
 
163
172
  /**
@@ -1131,16 +1140,26 @@ export class Channel {
1131
1140
  }
1132
1141
 
1133
1142
  /**
1134
- * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled
1143
+ * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled. Syncs the message delivery report candidates local state.
1135
1144
  *
1136
1145
  * @param {MarkReadOptions} data
1137
1146
  * @return {Promise<EventAPIResponse | null>} Description
1138
1147
  */
1139
1148
  async markRead(data: MarkReadOptions = {}) {
1149
+ return await this.getClient().messageDeliveryReporter.markRead(this, data);
1150
+ }
1151
+
1152
+ /**
1153
+ * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is enabled
1154
+ *
1155
+ * @param {MarkReadOptions} data
1156
+ * @return {Promise<EventAPIResponse | null>} Description
1157
+ */
1158
+ async markAsReadRequest(data: MarkReadOptions = {}) {
1140
1159
  this._checkInitialized();
1141
1160
 
1142
1161
  if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
1143
- return Promise.resolve(null);
1162
+ return null;
1144
1163
  }
1145
1164
 
1146
1165
  return await this.getClient().post<EventAPIResponse>(this._channelURL() + '/read', {
@@ -1554,6 +1573,7 @@ export class Channel {
1554
1573
  { method: 'upsertChannels' },
1555
1574
  );
1556
1575
 
1576
+ this.getClient().syncDeliveredCandidates([this]);
1557
1577
  return state;
1558
1578
  }
1559
1579
 
@@ -1874,18 +1894,50 @@ export class Channel {
1874
1894
  break;
1875
1895
  case 'message.read':
1876
1896
  if (event.user?.id && event.created_at) {
1897
+ const previousReadState = channelState.read[event.user.id];
1877
1898
  channelState.read[event.user.id] = {
1899
+ // in case we already have delivery information
1900
+ ...previousReadState,
1878
1901
  last_read: new Date(event.created_at),
1879
1902
  last_read_message_id: event.last_read_message_id,
1880
1903
  user: event.user,
1881
1904
  unread_messages: 0,
1882
1905
  };
1906
+ this.messageReceiptsTracker.onMessageRead({
1907
+ user: event.user,
1908
+ readAt: event.created_at,
1909
+ lastReadMessageId: event.last_read_message_id,
1910
+ });
1911
+ const client = this.getClient();
1883
1912
 
1884
- if (event.user?.id === this.getClient().user?.id) {
1913
+ const isOwnEvent = event.user?.id === client.user?.id;
1914
+
1915
+ if (isOwnEvent) {
1885
1916
  channelState.unreadCount = 0;
1917
+ client.syncDeliveredCandidates([this]);
1886
1918
  }
1887
1919
  }
1888
1920
  break;
1921
+ case 'message.delivered':
1922
+ // todo: update also on thread
1923
+ if (event.user?.id && event.created_at) {
1924
+ const previousReadState = channelState.read[event.user.id];
1925
+ channelState.read[event.user.id] = {
1926
+ ...previousReadState,
1927
+ last_delivered_at: event.last_delivered_at
1928
+ ? new Date(event.last_delivered_at)
1929
+ : undefined,
1930
+ last_delivered_message_id: event.last_delivered_message_id,
1931
+ user: event.user,
1932
+ };
1933
+
1934
+ this.messageReceiptsTracker.onMessageDelivered({
1935
+ user: event.user,
1936
+ deliveredAt: event.created_at,
1937
+ lastDeliveredMessageId: event.last_delivered_message_id,
1938
+ });
1939
+ }
1940
+ break;
1889
1941
  case 'user.watching.start':
1890
1942
  case 'user.updated':
1891
1943
  if (event.user?.id) {
@@ -1921,8 +1973,9 @@ export class Channel {
1921
1973
  break;
1922
1974
  case 'message.new':
1923
1975
  if (event.message) {
1976
+ const client = this.getClient();
1924
1977
  /* if message belongs to current user, always assume timestamp is changed to filter it out and add again to avoid duplication */
1925
- const ownMessage = event.user?.id === this.getClient().user?.id;
1978
+ const ownMessage = event.user?.id === client.user?.id;
1926
1979
  const isThreadMessage =
1927
1980
  event.message.parent_id && !event.message.show_in_channel;
1928
1981
 
@@ -1947,6 +2000,8 @@ export class Channel {
1947
2000
  last_read: new Date(event.created_at as string),
1948
2001
  user: event.user,
1949
2002
  unread_messages: 0,
2003
+ last_delivered_at: new Date(event.created_at as string),
2004
+ last_delivered_message_id: event.message.id,
1950
2005
  };
1951
2006
  } else {
1952
2007
  channelState.read[userId].unread_messages += 1;
@@ -1957,6 +2012,8 @@ export class Channel {
1957
2012
  if (this._countMessageAsUnread(event.message)) {
1958
2013
  channelState.unreadCount = channelState.unreadCount + 1;
1959
2014
  }
2015
+
2016
+ client.syncDeliveredCandidates([this]);
1960
2017
  }
1961
2018
  break;
1962
2019
  case 'message.updated':
@@ -2057,11 +2114,13 @@ export class Channel {
2057
2114
  break;
2058
2115
  case 'notification.mark_unread': {
2059
2116
  const ownMessage = event.user?.id === this.getClient().user?.id;
2060
- if (!(ownMessage && event.user)) break;
2117
+ if (!ownMessage || !event.user) break;
2061
2118
 
2062
2119
  const unreadCount = event.unread_messages ?? 0;
2063
-
2120
+ const currentState = channelState.read[event.user.id];
2064
2121
  channelState.read[event.user.id] = {
2122
+ // keep the message delivery info
2123
+ ...currentState,
2065
2124
  first_unread_message_id: event.first_unread_message_id,
2066
2125
  last_read: new Date(event.last_read_at as string),
2067
2126
  last_read_message_id: event.last_read_message_id,
@@ -2070,6 +2129,11 @@ export class Channel {
2070
2129
  };
2071
2130
 
2072
2131
  channelState.unreadCount = unreadCount;
2132
+ this.messageReceiptsTracker.onNotificationMarkUnread({
2133
+ user: event.user,
2134
+ lastReadAt: event.last_read_at,
2135
+ lastReadMessageId: event.last_read_message_id,
2136
+ });
2073
2137
  break;
2074
2138
  }
2075
2139
  case 'channel.updated':
@@ -2286,6 +2350,8 @@ export class Channel {
2286
2350
  this.state.unreadCount = this.state.read[read.user.id].unread_messages;
2287
2351
  }
2288
2352
  }
2353
+
2354
+ this.messageReceiptsTracker.ingestInitial(state.read);
2289
2355
  }
2290
2356
 
2291
2357
  return {