stream-chat 9.20.3 → 9.22.0
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/dist/cjs/index.browser.js +674 -42
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +676 -42
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +674 -42
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/channel.d.ts +11 -2
- package/dist/types/channel_state.d.ts +8 -0
- package/dist/types/client.d.ts +22 -3
- package/dist/types/events.d.ts +5 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messageDelivery/MessageDeliveryReporter.d.ts +74 -0
- package/dist/types/messageDelivery/MessageReceiptsTracker.d.ts +121 -0
- package/dist/types/messageDelivery/index.d.ts +2 -0
- package/dist/types/types.d.ts +31 -4
- package/dist/types/utils.d.ts +2 -1
- package/package.json +1 -1
- package/src/channel.ts +77 -7
- package/src/channel_state.ts +149 -14
- package/src/client.ts +73 -8
- package/src/events.ts +5 -6
- package/src/index.ts +1 -0
- package/src/messageComposer/messageComposer.ts +3 -6
- package/src/messageComposer/textComposer.ts +9 -3
- package/src/messageDelivery/MessageDeliveryReporter.ts +259 -0
- package/src/messageDelivery/MessageReceiptsTracker.ts +417 -0
- package/src/messageDelivery/index.ts +2 -0
- package/src/thread.ts +1 -1
- package/src/types.ts +36 -5
- package/src/utils.ts +2 -1
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
|
|
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
|
-
|
|
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 ===
|
|
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 (!
|
|
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':
|
|
@@ -2272,6 +2336,10 @@ export class Channel {
|
|
|
2272
2336
|
if (state.read) {
|
|
2273
2337
|
for (const read of state.read) {
|
|
2274
2338
|
this.state.read[read.user.id] = {
|
|
2339
|
+
last_delivered_at: read.last_delivered_at
|
|
2340
|
+
? new Date(read.last_delivered_at)
|
|
2341
|
+
: undefined,
|
|
2342
|
+
last_delivered_message_id: read.last_delivered_message_id,
|
|
2275
2343
|
last_read: new Date(read.last_read),
|
|
2276
2344
|
last_read_message_id: read.last_read_message_id,
|
|
2277
2345
|
unread_messages: read.unread_messages ?? 0,
|
|
@@ -2282,6 +2350,8 @@ export class Channel {
|
|
|
2282
2350
|
this.state.unreadCount = this.state.read[read.user.id].unread_messages;
|
|
2283
2351
|
}
|
|
2284
2352
|
}
|
|
2353
|
+
|
|
2354
|
+
this.messageReceiptsTracker.ingestInitial(state.read);
|
|
2285
2355
|
}
|
|
2286
2356
|
|
|
2287
2357
|
return {
|
package/src/channel_state.ts
CHANGED
|
@@ -26,9 +26,43 @@ type ChannelReadStatus = Record<
|
|
|
26
26
|
user: UserResponse;
|
|
27
27
|
first_unread_message_id?: string;
|
|
28
28
|
last_read_message_id?: string;
|
|
29
|
+
last_delivered_at?: Date;
|
|
30
|
+
last_delivered_message_id?: string;
|
|
29
31
|
}
|
|
30
32
|
>;
|
|
31
33
|
|
|
34
|
+
const messageSetBounds = (
|
|
35
|
+
a: LocalMessage[] | MessageResponse[],
|
|
36
|
+
b: LocalMessage[] | MessageResponse[],
|
|
37
|
+
) => ({
|
|
38
|
+
newestMessageA: new Date(a[0]?.created_at ?? 0),
|
|
39
|
+
oldestMessageA: new Date(a.slice(-1)[0]?.created_at ?? 0),
|
|
40
|
+
newestMessageB: new Date(b[0]?.created_at ?? 0),
|
|
41
|
+
oldestMessageB: new Date(b.slice(-1)[0]?.created_at ?? 0),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const aContainsOrEqualsB = (a: LocalMessage[], b: LocalMessage[]) => {
|
|
45
|
+
const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } =
|
|
46
|
+
messageSetBounds(a, b);
|
|
47
|
+
return newestMessageA >= newestMessageB && oldestMessageB >= oldestMessageA;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const aOverlapsB = (a: LocalMessage[], b: LocalMessage[]) => {
|
|
51
|
+
const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } =
|
|
52
|
+
messageSetBounds(a, b);
|
|
53
|
+
return (
|
|
54
|
+
oldestMessageA < oldestMessageB &&
|
|
55
|
+
oldestMessageB < newestMessageA &&
|
|
56
|
+
newestMessageA < newestMessageB
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const messageSetsOverlapByTimestamp = (a: LocalMessage[], b: LocalMessage[]) =>
|
|
61
|
+
aContainsOrEqualsB(a, b) ||
|
|
62
|
+
aContainsOrEqualsB(b, a) ||
|
|
63
|
+
aOverlapsB(a, b) ||
|
|
64
|
+
aOverlapsB(b, a);
|
|
65
|
+
|
|
32
66
|
/**
|
|
33
67
|
* ChannelState - A container class for the channel state.
|
|
34
68
|
*/
|
|
@@ -865,6 +899,41 @@ export class ChannelState {
|
|
|
865
899
|
return this.messageSets[messageSetIndex].messages.find((m) => m.id === messageId);
|
|
866
900
|
}
|
|
867
901
|
|
|
902
|
+
findMessageByTimestamp(
|
|
903
|
+
timestampMs: number,
|
|
904
|
+
parentMessageId?: string,
|
|
905
|
+
exactTsMatch: boolean = false,
|
|
906
|
+
): LocalMessage | null {
|
|
907
|
+
if (
|
|
908
|
+
(parentMessageId && !this.threads[parentMessageId]) ||
|
|
909
|
+
this.messageSets.length === 0
|
|
910
|
+
)
|
|
911
|
+
return null;
|
|
912
|
+
const setIndex = this.findMessageSetByOldestTimestamp(timestampMs);
|
|
913
|
+
const targetMsgSet = this.messageSets[setIndex]?.messages;
|
|
914
|
+
if (!targetMsgSet?.length) return null;
|
|
915
|
+
const firstMsgTimestamp = targetMsgSet[0].created_at.getTime();
|
|
916
|
+
const lastMsgTimestamp = targetMsgSet.slice(-1)[0].created_at.getTime();
|
|
917
|
+
const isOutOfBound =
|
|
918
|
+
timestampMs < firstMsgTimestamp || lastMsgTimestamp < timestampMs;
|
|
919
|
+
if (isOutOfBound && exactTsMatch) return null;
|
|
920
|
+
|
|
921
|
+
let msgIndex = 0,
|
|
922
|
+
hi = targetMsgSet.length - 1;
|
|
923
|
+
while (msgIndex < hi) {
|
|
924
|
+
const mid = (msgIndex + hi) >>> 1;
|
|
925
|
+
if (timestampMs <= targetMsgSet[mid].created_at.getTime()) hi = mid;
|
|
926
|
+
else msgIndex = mid + 1;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const foundMessage = targetMsgSet[msgIndex];
|
|
930
|
+
return !exactTsMatch
|
|
931
|
+
? foundMessage
|
|
932
|
+
: foundMessage.created_at.getTime() === timestampMs
|
|
933
|
+
? foundMessage
|
|
934
|
+
: null;
|
|
935
|
+
}
|
|
936
|
+
|
|
868
937
|
private switchToMessageSet(index: number) {
|
|
869
938
|
const currentMessages = this.messageSets.find((s) => s.isCurrent);
|
|
870
939
|
if (!currentMessages) {
|
|
@@ -887,6 +956,26 @@ export class ChannelState {
|
|
|
887
956
|
);
|
|
888
957
|
}
|
|
889
958
|
|
|
959
|
+
/**
|
|
960
|
+
* Identifies the set index into which a message set would pertain if its first item's creation date corresponded to oldestTimestampMs.
|
|
961
|
+
* @param oldestTimestampMs
|
|
962
|
+
*/
|
|
963
|
+
private findMessageSetByOldestTimestamp = (oldestTimestampMs: number): number => {
|
|
964
|
+
let lo = 0,
|
|
965
|
+
hi = this.messageSets.length;
|
|
966
|
+
while (lo < hi) {
|
|
967
|
+
const mid = (lo + hi) >>> 1;
|
|
968
|
+
const msgSet = this.messageSets[mid];
|
|
969
|
+
// should not happen
|
|
970
|
+
if (msgSet.messages.length === 0) return -1;
|
|
971
|
+
|
|
972
|
+
const oldestMessageTimestampInSet = msgSet.messages[0].created_at.getTime();
|
|
973
|
+
if (oldestMessageTimestampInSet <= oldestTimestampMs) hi = mid;
|
|
974
|
+
else lo = mid + 1;
|
|
975
|
+
}
|
|
976
|
+
return lo;
|
|
977
|
+
};
|
|
978
|
+
|
|
890
979
|
private findTargetMessageSet(
|
|
891
980
|
newMessages: (MessageResponse | LocalMessage)[],
|
|
892
981
|
addIfDoesNotExist = true,
|
|
@@ -894,39 +983,85 @@ export class ChannelState {
|
|
|
894
983
|
) {
|
|
895
984
|
let messagesToAdd: (MessageResponse | LocalMessage)[] = newMessages;
|
|
896
985
|
let targetMessageSetIndex!: number;
|
|
986
|
+
if (newMessages.length === 0)
|
|
987
|
+
return { targetMessageSetIndex: 0, messagesToAdd: newMessages };
|
|
897
988
|
if (addIfDoesNotExist) {
|
|
898
|
-
const
|
|
989
|
+
const overlappingMessageSetIndicesByMsgIds = this.messageSets
|
|
899
990
|
.map((_, i) => i)
|
|
900
991
|
.filter((i) =>
|
|
901
992
|
this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages),
|
|
902
993
|
);
|
|
994
|
+
const overlappingMessageSetIndicesByTimestamp = this.messageSets
|
|
995
|
+
.map((_, i) => i)
|
|
996
|
+
.filter((i) =>
|
|
997
|
+
messageSetsOverlapByTimestamp(
|
|
998
|
+
this.messageSets[i].messages,
|
|
999
|
+
newMessages.map(formatMessage),
|
|
1000
|
+
),
|
|
1001
|
+
);
|
|
903
1002
|
switch (messageSetToAddToIfDoesNotExist) {
|
|
904
1003
|
case 'new':
|
|
905
|
-
if (
|
|
906
|
-
targetMessageSetIndex =
|
|
1004
|
+
if (overlappingMessageSetIndicesByMsgIds.length > 0) {
|
|
1005
|
+
targetMessageSetIndex = overlappingMessageSetIndicesByMsgIds[0];
|
|
1006
|
+
} else if (overlappingMessageSetIndicesByTimestamp.length > 0) {
|
|
1007
|
+
targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
|
|
907
1008
|
// No new message set is created if newMessages only contains thread replies
|
|
908
1009
|
} else if (newMessages.some((m) => !m.parent_id)) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1010
|
+
// find the index to insert the set
|
|
1011
|
+
const setIngestIndex = this.findMessageSetByOldestTimestamp(
|
|
1012
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1013
|
+
new Date(newMessages[0].created_at!).getTime(),
|
|
1014
|
+
);
|
|
1015
|
+
if (setIngestIndex === -1) {
|
|
1016
|
+
this.messageSets.push({
|
|
1017
|
+
messages: [],
|
|
1018
|
+
isCurrent: false,
|
|
1019
|
+
isLatest: false,
|
|
1020
|
+
pagination: DEFAULT_MESSAGE_SET_PAGINATION,
|
|
1021
|
+
});
|
|
1022
|
+
targetMessageSetIndex = this.messageSets.length - 1;
|
|
1023
|
+
} else {
|
|
1024
|
+
const isLatest = setIngestIndex === 0;
|
|
1025
|
+
this.messageSets.splice(setIngestIndex, 0, {
|
|
1026
|
+
messages: [],
|
|
1027
|
+
isCurrent: false,
|
|
1028
|
+
isLatest,
|
|
1029
|
+
pagination: DEFAULT_MESSAGE_SET_PAGINATION, // fixme: it is problematic decide about pagination without having data
|
|
1030
|
+
});
|
|
1031
|
+
if (isLatest) {
|
|
1032
|
+
this.messageSets.slice(1).forEach((set) => {
|
|
1033
|
+
set.isLatest = false;
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
targetMessageSetIndex = setIngestIndex;
|
|
1037
|
+
}
|
|
916
1038
|
}
|
|
917
1039
|
break;
|
|
918
1040
|
case 'current':
|
|
919
|
-
|
|
1041
|
+
// determine if there is another set to which it would match taken into consideration the timestamp
|
|
1042
|
+
if (overlappingMessageSetIndicesByTimestamp.length > 0) {
|
|
1043
|
+
targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
|
|
1044
|
+
} else {
|
|
1045
|
+
targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
|
|
1046
|
+
}
|
|
920
1047
|
break;
|
|
921
1048
|
case 'latest':
|
|
922
|
-
|
|
1049
|
+
// determine if there is another set to which it would match taken into consideration the timestamp
|
|
1050
|
+
if (overlappingMessageSetIndicesByTimestamp.length > 0) {
|
|
1051
|
+
targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
|
|
1052
|
+
} else {
|
|
1053
|
+
targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
|
|
1054
|
+
}
|
|
923
1055
|
break;
|
|
924
1056
|
default:
|
|
925
1057
|
targetMessageSetIndex = -1;
|
|
926
1058
|
}
|
|
927
1059
|
// when merging the target set will be the first one from the overlapping message sets
|
|
928
|
-
const mergeTargetMessageSetIndex =
|
|
929
|
-
|
|
1060
|
+
const mergeTargetMessageSetIndex = overlappingMessageSetIndicesByMsgIds.splice(
|
|
1061
|
+
0,
|
|
1062
|
+
1,
|
|
1063
|
+
)[0];
|
|
1064
|
+
const mergeSourceMessageSetIndices = [...overlappingMessageSetIndicesByMsgIds];
|
|
930
1065
|
if (
|
|
931
1066
|
mergeTargetMessageSetIndex !== undefined &&
|
|
932
1067
|
mergeTargetMessageSetIndex !== targetMessageSetIndex
|
package/src/client.ts
CHANGED
|
@@ -80,6 +80,7 @@ import type {
|
|
|
80
80
|
DeactivateUsersOptions,
|
|
81
81
|
DeleteChannelsResponse,
|
|
82
82
|
DeleteCommandResponse,
|
|
83
|
+
DeleteMessageOptions,
|
|
83
84
|
DeleteUserOptions,
|
|
84
85
|
Device,
|
|
85
86
|
DeviceIdentifier,
|
|
@@ -87,6 +88,7 @@ import type {
|
|
|
87
88
|
DraftSort,
|
|
88
89
|
EndpointName,
|
|
89
90
|
Event,
|
|
91
|
+
EventAPIResponse,
|
|
90
92
|
EventHandler,
|
|
91
93
|
ExportChannelOptions,
|
|
92
94
|
ExportChannelRequest,
|
|
@@ -124,6 +126,7 @@ import type {
|
|
|
124
126
|
LocalMessage,
|
|
125
127
|
Logger,
|
|
126
128
|
MarkChannelsReadOptions,
|
|
129
|
+
MarkDeliveredOptions,
|
|
127
130
|
MessageFilters,
|
|
128
131
|
MessageFlagsFilters,
|
|
129
132
|
MessageFlagsPaginationOptions,
|
|
@@ -238,6 +241,7 @@ import type {
|
|
|
238
241
|
QueryChannelsRequestType,
|
|
239
242
|
} from './channel_manager';
|
|
240
243
|
import { ChannelManager } from './channel_manager';
|
|
244
|
+
import { MessageDeliveryReporter } from './messageDelivery';
|
|
241
245
|
import { NotificationManager } from './notifications';
|
|
242
246
|
import { ReminderManager } from './reminders';
|
|
243
247
|
import { StateStore } from './store';
|
|
@@ -270,7 +274,7 @@ export type MessageComposerSetupState = {
|
|
|
270
274
|
|
|
271
275
|
export class StreamChat {
|
|
272
276
|
private static _instance?: unknown | StreamChat; // type is undefined|StreamChat, unknown is due to TS limitations with statics
|
|
273
|
-
|
|
277
|
+
messageDeliveryReporter: MessageDeliveryReporter;
|
|
274
278
|
_user?: OwnUserResponse | UserResponse;
|
|
275
279
|
appSettingsPromise?: Promise<AppSettingsAPIResponse>;
|
|
276
280
|
activeChannels: {
|
|
@@ -501,6 +505,7 @@ export class StreamChat {
|
|
|
501
505
|
this.threads = new ThreadManager({ client: this });
|
|
502
506
|
this.polls = new PollManager({ client: this });
|
|
503
507
|
this.reminders = new ReminderManager({ client: this });
|
|
508
|
+
this.messageDeliveryReporter = new MessageDeliveryReporter({ client: this });
|
|
504
509
|
}
|
|
505
510
|
|
|
506
511
|
/**
|
|
@@ -2008,7 +2013,7 @@ export class StreamChat {
|
|
|
2008
2013
|
|
|
2009
2014
|
channels.push(c);
|
|
2010
2015
|
}
|
|
2011
|
-
|
|
2016
|
+
this.syncDeliveredCandidates(channels);
|
|
2012
2017
|
return channels;
|
|
2013
2018
|
}
|
|
2014
2019
|
|
|
@@ -3101,10 +3106,30 @@ export class StreamChat {
|
|
|
3101
3106
|
);
|
|
3102
3107
|
}
|
|
3103
3108
|
|
|
3104
|
-
|
|
3109
|
+
/**
|
|
3110
|
+
* deleteMessage - Delete a message
|
|
3111
|
+
*
|
|
3112
|
+
* @param {string} messageID The id of the message to delete
|
|
3113
|
+
* @param {boolean | DeleteMessageOptions | undefined} [optionsOrHardDelete]
|
|
3114
|
+
* @return {Promise<APIResponse & { message: MessageResponse }>} The API response
|
|
3115
|
+
*/
|
|
3116
|
+
// fixme: remove the signature with optionsOrHardDelete boolean with the next major release
|
|
3117
|
+
async deleteMessage(
|
|
3118
|
+
messageID: string,
|
|
3119
|
+
optionsOrHardDelete?: DeleteMessageOptions | boolean,
|
|
3120
|
+
): Promise<APIResponse & { message: MessageResponse }> {
|
|
3121
|
+
let options: DeleteMessageOptions = {};
|
|
3122
|
+
if (typeof optionsOrHardDelete === 'boolean') {
|
|
3123
|
+
options = optionsOrHardDelete ? { hardDelete: true } : {};
|
|
3124
|
+
} else if (optionsOrHardDelete?.deleteForMe) {
|
|
3125
|
+
options = { deleteForMe: true };
|
|
3126
|
+
} else if (optionsOrHardDelete?.hardDelete) {
|
|
3127
|
+
options = { hardDelete: true };
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3105
3130
|
try {
|
|
3106
3131
|
if (this.offlineDb) {
|
|
3107
|
-
if (hardDelete) {
|
|
3132
|
+
if (options.hardDelete) {
|
|
3108
3133
|
await this.offlineDb.hardDeleteMessage({ id: messageID });
|
|
3109
3134
|
} else {
|
|
3110
3135
|
await this.offlineDb.softDeleteMessage({ id: messageID });
|
|
@@ -3113,7 +3138,7 @@ export class StreamChat {
|
|
|
3113
3138
|
{
|
|
3114
3139
|
task: {
|
|
3115
3140
|
messageId: messageID,
|
|
3116
|
-
payload: [messageID,
|
|
3141
|
+
payload: [messageID, options],
|
|
3117
3142
|
type: 'delete-message',
|
|
3118
3143
|
},
|
|
3119
3144
|
},
|
|
@@ -3126,18 +3151,40 @@ export class StreamChat {
|
|
|
3126
3151
|
});
|
|
3127
3152
|
}
|
|
3128
3153
|
|
|
3129
|
-
return this._deleteMessage(messageID,
|
|
3154
|
+
return this._deleteMessage(messageID, options);
|
|
3130
3155
|
}
|
|
3131
3156
|
|
|
3132
|
-
|
|
3157
|
+
// fixme: remove the signature with optionsOrHardDelete boolean with the next major release
|
|
3158
|
+
async _deleteMessage(
|
|
3159
|
+
messageID: string,
|
|
3160
|
+
optionsOrHardDelete?: DeleteMessageOptions | boolean,
|
|
3161
|
+
): Promise<APIResponse & { message: MessageResponse }> {
|
|
3162
|
+
// this is a API call method, we do not route hardDelete: true and deleteForMe: true to deleteForMe: true
|
|
3163
|
+
// and expect to receive error response from the server
|
|
3164
|
+
const { deleteForMe, hardDelete } = (
|
|
3165
|
+
typeof optionsOrHardDelete === 'boolean'
|
|
3166
|
+
? { hardDelete: optionsOrHardDelete }
|
|
3167
|
+
: (optionsOrHardDelete ?? {})
|
|
3168
|
+
) as DeleteMessageOptions;
|
|
3169
|
+
|
|
3133
3170
|
let params = {};
|
|
3134
3171
|
if (hardDelete) {
|
|
3135
3172
|
params = { hard: true };
|
|
3136
3173
|
}
|
|
3137
|
-
|
|
3174
|
+
if (deleteForMe) {
|
|
3175
|
+
params = { ...params, delete_for_me: true };
|
|
3176
|
+
}
|
|
3177
|
+
const result = await this.delete<APIResponse & { message: MessageResponse }>(
|
|
3138
3178
|
this.baseURL + `/messages/${encodeURIComponent(messageID)}`,
|
|
3139
3179
|
params,
|
|
3140
3180
|
);
|
|
3181
|
+
|
|
3182
|
+
// necessary to populate the below values as the server does not return the message in the response as deleted
|
|
3183
|
+
if (deleteForMe) {
|
|
3184
|
+
result.message.deleted_for_me = true;
|
|
3185
|
+
result.message.type = 'deleted';
|
|
3186
|
+
}
|
|
3187
|
+
return result;
|
|
3141
3188
|
}
|
|
3142
3189
|
|
|
3143
3190
|
/**
|
|
@@ -4697,4 +4744,22 @@ export class StreamChat {
|
|
|
4697
4744
|
deleteImage(url: string) {
|
|
4698
4745
|
return this.delete<APIResponse>(`${this.baseURL}/uploads/image`, { url });
|
|
4699
4746
|
}
|
|
4747
|
+
|
|
4748
|
+
/**
|
|
4749
|
+
* Send the mark delivered event for this user
|
|
4750
|
+
*
|
|
4751
|
+
* @param {MarkDeliveredOptions} data
|
|
4752
|
+
* @return {Promise<EventAPIResponse | void>} Description
|
|
4753
|
+
*/
|
|
4754
|
+
async markChannelsDelivered(data: MarkDeliveredOptions) {
|
|
4755
|
+
if (!data?.latest_delivered_messages?.length) return;
|
|
4756
|
+
return await this.post<EventAPIResponse>(
|
|
4757
|
+
this.baseURL + '/channels/delivered',
|
|
4758
|
+
data ?? {},
|
|
4759
|
+
);
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
syncDeliveredCandidates(collections: Channel[]) {
|
|
4763
|
+
this.messageDeliveryReporter.syncDeliveredCandidates(collections);
|
|
4764
|
+
}
|
|
4700
4765
|
}
|
package/src/events.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const EVENT_MAP = {
|
|
|
21
21
|
'message.undeleted': true,
|
|
22
22
|
'notification.added_to_channel': true,
|
|
23
23
|
'notification.channel_deleted': true,
|
|
24
|
+
'message.delivered': true,
|
|
24
25
|
'notification.channel_mutes_updated': true,
|
|
25
26
|
'notification.channel_truncated': true,
|
|
26
27
|
'notification.invite_accepted': true,
|
|
@@ -30,6 +31,7 @@ export const EVENT_MAP = {
|
|
|
30
31
|
'notification.mark_unread': true,
|
|
31
32
|
'notification.message_new': true,
|
|
32
33
|
'notification.mutes_updated': true,
|
|
34
|
+
'notification.reminder_due': true,
|
|
33
35
|
'notification.removed_from_channel': true,
|
|
34
36
|
'notification.thread_message_new': true,
|
|
35
37
|
'poll.closed': true,
|
|
@@ -40,6 +42,9 @@ export const EVENT_MAP = {
|
|
|
40
42
|
'reaction.deleted': true,
|
|
41
43
|
'reaction.new': true,
|
|
42
44
|
'reaction.updated': true,
|
|
45
|
+
'reminder.created': true,
|
|
46
|
+
'reminder.deleted': true,
|
|
47
|
+
'reminder.updated': true,
|
|
43
48
|
'thread.updated': true,
|
|
44
49
|
'typing.start': true,
|
|
45
50
|
'typing.stop': true,
|
|
@@ -66,10 +71,4 @@ export const EVENT_MAP = {
|
|
|
66
71
|
'capabilities.changed': true,
|
|
67
72
|
'live_location_sharing.started': true,
|
|
68
73
|
'live_location_sharing.stopped': true,
|
|
69
|
-
|
|
70
|
-
// Reminder events
|
|
71
|
-
'reminder.created': true,
|
|
72
|
-
'reminder.updated': true,
|
|
73
|
-
'reminder.deleted': true,
|
|
74
|
-
'notification.reminder_due': true,
|
|
75
74
|
};
|
package/src/index.ts
CHANGED
|
@@ -812,12 +812,9 @@ export class MessageComposer extends WithSubscriptions {
|
|
|
812
812
|
|
|
813
813
|
this.initState({ composition: draft });
|
|
814
814
|
} catch (error) {
|
|
815
|
-
this.client.
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
emitter: 'MessageComposer',
|
|
819
|
-
context: { composer: this },
|
|
820
|
-
},
|
|
815
|
+
this.client.logger('error', `messageComposer:getDraft`, {
|
|
816
|
+
tags: ['channel', 'messageComposer'],
|
|
817
|
+
error,
|
|
821
818
|
});
|
|
822
819
|
}
|
|
823
820
|
};
|
|
@@ -203,13 +203,19 @@ export class TextComposer {
|
|
|
203
203
|
selection?: TextSelection;
|
|
204
204
|
}) => {
|
|
205
205
|
if (!this.enabled) return;
|
|
206
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Windows inserts \r\n; macOS inserts \n.
|
|
208
|
+
* The caret can fall inside a CRLF pair during repeated pastes on Windows.
|
|
209
|
+
* That corrupts newline alignment (a\nb\na\nba\nb\n\n).
|
|
210
|
+
* Normalize the text to prevent it.
|
|
211
|
+
*/
|
|
212
|
+
const normalizedText = text.replace(/\r\n/g, '\n');
|
|
207
213
|
const finalSelection: TextSelection = selection ?? this.selection;
|
|
208
214
|
const { maxLengthOnEdit } = this.composer.config.text ?? {};
|
|
209
215
|
const currentText = this.text;
|
|
210
216
|
const textBeforeTrim = [
|
|
211
217
|
currentText.slice(0, finalSelection.start),
|
|
212
|
-
|
|
218
|
+
normalizedText,
|
|
213
219
|
currentText.slice(finalSelection.end),
|
|
214
220
|
].join('');
|
|
215
221
|
|
|
@@ -217,7 +223,7 @@ export class TextComposer {
|
|
|
217
223
|
0,
|
|
218
224
|
typeof maxLengthOnEdit === 'number' ? maxLengthOnEdit : textBeforeTrim.length,
|
|
219
225
|
);
|
|
220
|
-
const expectedCursorPosition = finalSelection.start +
|
|
226
|
+
const expectedCursorPosition = finalSelection.start + normalizedText.length;
|
|
221
227
|
const cursorPosition = Math.min(expectedCursorPosition, finalText.length);
|
|
222
228
|
|
|
223
229
|
await this.handleChange({
|