Skip to content

Commit 3a870dc

Browse files
committed
Merge branch 'master' into feat/message-pruning-api
# Conflicts: # src/channel_state.ts
2 parents a702338 + f0e8646 commit 3a870dc

19 files changed

+2690
-155
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [9.22.0](https://github.com/GetStream/stream-chat-js/compare/v9.21.0...v9.22.0) (2025-10-09)
2+
3+
### Features
4+
5+
* add deleted_for_me field in message response ([#1604](https://github.com/GetStream/stream-chat-js/issues/1604)) ([26e83c4](https://github.com/GetStream/stream-chat-js/commit/26e83c45259bea48506ae107a49f22d5d4fafd95))
6+
* add message delivery receipts ([#1617](https://github.com/GetStream/stream-chat-js/issues/1617)) ([c8a2fe6](https://github.com/GetStream/stream-chat-js/commit/c8a2fe66f014b59b34f8c1391dc7c4d65b380765))
7+
18
## [9.21.0](https://github.com/GetStream/stream-chat-js/compare/v9.20.3...v9.21.0) (2025-10-08)
29

310
### Bug Fixes

src/channel.ts

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { ChannelState } from './channel_state';
2+
import { MessageComposer } from './messageComposer';
3+
import { MessageReceiptsTracker } from './messageDelivery';
24
import {
35
generateChannelTempCid,
46
logChatPromiseExecution,
@@ -74,7 +76,6 @@ import type {
7476
} from './types';
7577
import type { Role } from './permissions';
7678
import type { CustomChannelData } from './custom_types';
77-
import { MessageComposer } from './messageComposer';
7879

7980
/**
8081
* Channel - The Channel class manages it's own state.
@@ -110,6 +111,7 @@ export class Channel {
110111
disconnected: boolean;
111112
push_preferences?: PushPreference;
112113
public readonly messageComposer: MessageComposer;
114+
public readonly messageReceiptsTracker: MessageReceiptsTracker;
113115

114116
/**
115117
* constructor - Create a channel
@@ -158,6 +160,13 @@ export class Channel {
158160
client: this._client,
159161
compositionContext: this,
160162
});
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+
});
161170
}
162171

163172
/**
@@ -1131,16 +1140,26 @@ export class Channel {
11311140
}
11321141

11331142
/**
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.
11351144
*
11361145
* @param {MarkReadOptions} data
11371146
* @return {Promise<EventAPIResponse | null>} Description
11381147
*/
11391148
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 = {}) {
11401159
this._checkInitialized();
11411160

11421161
if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
1143-
return Promise.resolve(null);
1162+
return null;
11441163
}
11451164

11461165
return await this.getClient().post<EventAPIResponse>(this._channelURL() + '/read', {
@@ -1554,6 +1573,7 @@ export class Channel {
15541573
{ method: 'upsertChannels' },
15551574
);
15561575

1576+
this.getClient().syncDeliveredCandidates([this]);
15571577
return state;
15581578
}
15591579

@@ -1874,18 +1894,50 @@ export class Channel {
18741894
break;
18751895
case 'message.read':
18761896
if (event.user?.id && event.created_at) {
1897+
const previousReadState = channelState.read[event.user.id];
18771898
channelState.read[event.user.id] = {
1899+
// in case we already have delivery information
1900+
...previousReadState,
18781901
last_read: new Date(event.created_at),
18791902
last_read_message_id: event.last_read_message_id,
18801903
user: event.user,
18811904
unread_messages: 0,
18821905
};
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();
18831912

1884-
if (event.user?.id === this.getClient().user?.id) {
1913+
const isOwnEvent = event.user?.id === client.user?.id;
1914+
1915+
if (isOwnEvent) {
18851916
channelState.unreadCount = 0;
1917+
client.syncDeliveredCandidates([this]);
18861918
}
18871919
}
18881920
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;
18891941
case 'user.watching.start':
18901942
case 'user.updated':
18911943
if (event.user?.id) {
@@ -1921,8 +1973,9 @@ export class Channel {
19211973
break;
19221974
case 'message.new':
19231975
if (event.message) {
1976+
const client = this.getClient();
19241977
/* 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;
19261979
const isThreadMessage =
19271980
event.message.parent_id && !event.message.show_in_channel;
19281981

@@ -1947,6 +2000,8 @@ export class Channel {
19472000
last_read: new Date(event.created_at as string),
19482001
user: event.user,
19492002
unread_messages: 0,
2003+
last_delivered_at: new Date(event.created_at as string),
2004+
last_delivered_message_id: event.message.id,
19502005
};
19512006
} else {
19522007
channelState.read[userId].unread_messages += 1;
@@ -1957,6 +2012,8 @@ export class Channel {
19572012
if (this._countMessageAsUnread(event.message)) {
19582013
channelState.unreadCount = channelState.unreadCount + 1;
19592014
}
2015+
2016+
client.syncDeliveredCandidates([this]);
19602017
}
19612018
break;
19622019
case 'message.updated':
@@ -2057,11 +2114,13 @@ export class Channel {
20572114
break;
20582115
case 'notification.mark_unread': {
20592116
const ownMessage = event.user?.id === this.getClient().user?.id;
2060-
if (!(ownMessage && event.user)) break;
2117+
if (!ownMessage || !event.user) break;
20612118

20622119
const unreadCount = event.unread_messages ?? 0;
2063-
2120+
const currentState = channelState.read[event.user.id];
20642121
channelState.read[event.user.id] = {
2122+
// keep the message delivery info
2123+
...currentState,
20652124
first_unread_message_id: event.first_unread_message_id,
20662125
last_read: new Date(event.last_read_at as string),
20672126
last_read_message_id: event.last_read_message_id,
@@ -2070,6 +2129,11 @@ export class Channel {
20702129
};
20712130

20722131
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+
});
20732137
break;
20742138
}
20752139
case 'channel.updated':
@@ -2286,6 +2350,8 @@ export class Channel {
22862350
this.state.unreadCount = this.state.read[read.user.id].unread_messages;
22872351
}
22882352
}
2353+
2354+
this.messageReceiptsTracker.ingestInitial(state.read);
22892355
}
22902356

22912357
return {

0 commit comments

Comments
 (0)