stream-chat 9.48.0 → 9.50.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 +109 -48
- package/dist/cjs/index.browser.js.map +2 -2
- package/dist/cjs/index.node.js +109 -48
- package/dist/cjs/index.node.js.map +2 -2
- package/dist/esm/index.mjs +109 -48
- package/dist/esm/index.mjs.map +2 -2
- package/dist/types/channel.d.ts +12 -0
- package/dist/types/client.d.ts +4 -2
- package/dist/types/events.d.ts +1 -0
- package/dist/types/types.d.ts +16 -0
- package/dist/types/utils.d.ts +13 -0
- package/package.json +1 -1
- package/src/channel.ts +45 -4
- package/src/client.ts +47 -28
- package/src/events.ts +1 -0
- package/src/messageDelivery/MessageDeliveryReporter.ts +3 -1
- package/src/offline-support/offline_support_api.ts +53 -24
- package/src/types.ts +14 -0
- package/src/utils.ts +22 -0
package/dist/types/channel.d.ts
CHANGED
|
@@ -503,6 +503,18 @@ export declare class Channel {
|
|
|
503
503
|
* @return {APIResponse} An API response
|
|
504
504
|
*/
|
|
505
505
|
markUnread(data: MarkUnreadOptions): Promise<APIResponse | null>;
|
|
506
|
+
/**
|
|
507
|
+
* markReadLocally - Resets this user's unread count locally, without any backend call. Intended for
|
|
508
|
+
* channels that have read events disabled (e.g. livestreams) when the client is created with the
|
|
509
|
+
* `isLocalUnreadCountEnabled` option. Dispatches a dedicated, client-only `message.read_locally` event
|
|
510
|
+
* that runs through the same `_handleChannelEvent` read logic as a real `message.read` (minus the
|
|
511
|
+
* delivery-report network sync), so the read-state update lives in one place. When offline support
|
|
512
|
+
* is enabled, the offline DB persists the reset for read-events-disabled channels, so the local
|
|
513
|
+
* count stays consistent across app restarts.
|
|
514
|
+
*
|
|
515
|
+
* @return {Event | undefined} The dispatched `message.read_locally` event, or `undefined` if there is no connected user.
|
|
516
|
+
*/
|
|
517
|
+
markReadLocally(): Event | undefined;
|
|
506
518
|
/**
|
|
507
519
|
* clean - Cleans the channel state and fires stop typing if needed
|
|
508
520
|
*/
|
package/dist/types/client.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { WSConnectionFallback } from './connection_fallback';
|
|
|
8
8
|
import { Campaign } from './campaign';
|
|
9
9
|
import { ChannelBatchUpdater } from './channel_batch_updater';
|
|
10
10
|
import { Segment } from './segment';
|
|
11
|
-
import type { ActiveLiveLocationsAPIResponse, AddUserGroupMembersOptions, AddUserGroupMembersResponse, 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, CreatePredefinedFilterOptions, CreateReminderOptions, CreateRoleAPIResponse, CreateUserGroupOptions, CreateUserGroupResponse, CustomPermissionOptions, DeactivateUsersOptions, DeleteCommandResponse, DeleteMessageOptions, DeleteUserGroupOptions, DeleteUserOptions, Device, DeviceIdentifier, DraftFilters, DraftSort, EndpointName, Event, EventAPIResponse, EventHandler, ExportChannelOptions, ExportChannelRequest, ExportChannelResponse, ExportChannelStatusResponse, ExportUsersRequest, ExportUsersResponse, FlagMessageResponse, FlagReportsFilters, FlagReportsPaginationOptions, FlagReportsResponse, FlagsFilters, FlagsPaginationOptions, FlagsResponse, FlagUserResponse, FutureChannelBansResponse, GetBlockedUsersAPIResponse, GetCampaignOptions, GetChannelTypeResponse, GetCommandResponse, GetHookEventsResponse, GetImportResponse, GetMessageOptions, GetPollAPIResponse, GetRateLimitsResponse, GetRetentionPolicyResponse, GetRetentionPolicyRunsOptions, GetRetentionPolicyRunsResponse, GetThreadAPIResponse, GetThreadOptions, GetUnreadCountAPIResponse, GetUnreadCountBatchAPIResponse, GetUserGroupOptions, GetUserGroupResponse, ListChannelResponse, ListCommandsResponse, ListImportsPaginationOptions, ListImportsResponse, ListPredefinedFiltersOptions, ListPredefinedFiltersResponse, ListRolesAPIResponse, 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, PredefinedFilterResponse, Product, PushPreference, PushProvider, PushProviderConfig, PushProviderID, PushProviderListResponse, PushProviderUpsertResponse, QueryChannelsAPIResponse, QueryDraftsResponse, QueryFutureChannelBansOptions, QueryMessageHistoryFilters, QueryMessageHistoryOptions, QueryMessageHistoryResponse, QueryMessageHistorySort, QueryPollsFilters, QueryPollsOptions, QueryPollsResponse, QueryReactionsAPIResponse, QueryReactionsOptions, QueryRemindersOptions, QueryRemindersResponse, QuerySegmentsOptions, QuerySegmentTargetsFilter, QueryTeamUsageStatsOptions, QueryTeamUsageStatsResponse, QueryThreadsOptions, QueryUserGroupsOptions, QueryUserGroupsResponse, QueryVotesFilters, QueryVotesOptions, ReactionFilters, ReactionResponse, ReactionSort, ReactivateUserOptions, ReactivateUsersOptions, ReminderAPIResponse, RemoveUserGroupMembersOptions, RemoveUserGroupMembersResponse, ReviewFlagReportOptions, ReviewFlagReportResponse, SdkIdentifier, SearchAPIResponse, SearchOptions, SearchRolesAPIResponse, SearchRolesOptions, SearchUserGroupsOptions, SearchUserGroupsResponse, SegmentData, SegmentResponse, SegmentTargetsResponse, SegmentType, SendFileAPIResponse, SetRetentionPolicyResponse, SharedLocationResponse, SortParam, StreamChatOptions, SyncOptions, SyncResponse, TaskResponse, TaskStatus, TestPushDataInput, TestSNSDataInput, TestSQSDataInput, TokenOrProvider, TranslateResponse, UnBanUserOptions, UpdateChannelsBatchOptions, UpdateChannelTypeRequest, UpdateChannelTypeResponse, UpdateCommandOptions, UpdateCommandResponse, UpdateLocationPayload, UpdateMessageAPIResponse, UpdateMessageOptions, UpdatePollAPIResponse, UpdatePredefinedFilterOptions, UpdateReminderOptions, UpdateSegmentData, UpdateUserGroupOptions, UpdateUserGroupResponse, UpdateUsersAPIResponse, UpsertPushPreferencesResponse, UserCustomEvent, UserFilters, UserOptions, UserResponse, UserSort, VoteSort } from './types';
|
|
11
|
+
import type { ActiveLiveLocationsAPIResponse, AddUserGroupMembersOptions, AddUserGroupMembersResponse, APIErrorResponse, APIResponse, AppIdentifier, 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, CreatePredefinedFilterOptions, CreateReminderOptions, CreateRoleAPIResponse, CreateUserGroupOptions, CreateUserGroupResponse, CustomPermissionOptions, DeactivateUsersOptions, DeleteCommandResponse, DeleteMessageOptions, DeleteUserGroupOptions, DeleteUserOptions, Device, DeviceIdentifier, DraftFilters, DraftSort, EndpointName, Event, EventAPIResponse, EventHandler, ExportChannelOptions, ExportChannelRequest, ExportChannelResponse, ExportChannelStatusResponse, ExportUsersRequest, ExportUsersResponse, FlagMessageResponse, FlagReportsFilters, FlagReportsPaginationOptions, FlagReportsResponse, FlagsFilters, FlagsPaginationOptions, FlagsResponse, FlagUserResponse, FutureChannelBansResponse, GetBlockedUsersAPIResponse, GetCampaignOptions, GetChannelTypeResponse, GetCommandResponse, GetHookEventsResponse, GetImportResponse, GetMessageOptions, GetPollAPIResponse, GetRateLimitsResponse, GetRetentionPolicyResponse, GetRetentionPolicyRunsOptions, GetRetentionPolicyRunsResponse, GetThreadAPIResponse, GetThreadOptions, GetUnreadCountAPIResponse, GetUnreadCountBatchAPIResponse, GetUserGroupOptions, GetUserGroupResponse, ListChannelResponse, ListCommandsResponse, ListImportsPaginationOptions, ListImportsResponse, ListPredefinedFiltersOptions, ListPredefinedFiltersResponse, ListRolesAPIResponse, 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, PredefinedFilterResponse, Product, PushPreference, PushProvider, PushProviderConfig, PushProviderID, PushProviderListResponse, PushProviderUpsertResponse, QueryChannelsAPIResponse, QueryDraftsResponse, QueryFutureChannelBansOptions, QueryMessageHistoryFilters, QueryMessageHistoryOptions, QueryMessageHistoryResponse, QueryMessageHistorySort, QueryPollsFilters, QueryPollsOptions, QueryPollsResponse, QueryReactionsAPIResponse, QueryReactionsOptions, QueryRemindersOptions, QueryRemindersResponse, QuerySegmentsOptions, QuerySegmentTargetsFilter, QueryTeamUsageStatsOptions, QueryTeamUsageStatsResponse, QueryThreadsOptions, QueryUserGroupsOptions, QueryUserGroupsResponse, QueryVotesFilters, QueryVotesOptions, ReactionFilters, ReactionResponse, ReactionSort, ReactivateUserOptions, ReactivateUsersOptions, ReminderAPIResponse, RemoveUserGroupMembersOptions, RemoveUserGroupMembersResponse, ReviewFlagReportOptions, ReviewFlagReportResponse, SdkIdentifier, SearchAPIResponse, SearchOptions, SearchRolesAPIResponse, SearchRolesOptions, SearchUserGroupsOptions, SearchUserGroupsResponse, SegmentData, SegmentResponse, SegmentTargetsResponse, SegmentType, SendFileAPIResponse, SetRetentionPolicyResponse, SharedLocationResponse, SortParam, StreamChatOptions, SyncOptions, SyncResponse, TaskResponse, TaskStatus, TestPushDataInput, TestSNSDataInput, TestSQSDataInput, TokenOrProvider, TranslateResponse, UnBanUserOptions, UpdateChannelsBatchOptions, UpdateChannelTypeRequest, UpdateChannelTypeResponse, UpdateCommandOptions, UpdateCommandResponse, UpdateLocationPayload, UpdateMessageAPIResponse, UpdateMessageOptions, UpdatePollAPIResponse, UpdatePredefinedFilterOptions, UpdateReminderOptions, UpdateSegmentData, UpdateUserGroupOptions, UpdateUserGroupResponse, UpdateUsersAPIResponse, UpsertPushPreferencesResponse, UserCustomEvent, UserFilters, UserOptions, UserResponse, UserSort, VoteSort } from './types';
|
|
12
12
|
import { ErrorFromResponse } from './types';
|
|
13
13
|
import { InsightMetrics } from './insights';
|
|
14
14
|
import { Thread } from './thread';
|
|
@@ -111,6 +111,8 @@ export declare class StreamChat {
|
|
|
111
111
|
defaultWSTimeout: number;
|
|
112
112
|
sdkIdentifier?: SdkIdentifier;
|
|
113
113
|
deviceIdentifier?: DeviceIdentifier;
|
|
114
|
+
appIdentifier?: AppIdentifier;
|
|
115
|
+
private cachedUserAgent?;
|
|
114
116
|
readonly messageComposerCache: FixedSizeQueueCache<string, MessageComposer>;
|
|
115
117
|
private nextRequestAbortController;
|
|
116
118
|
/**
|
|
@@ -1319,7 +1321,7 @@ export declare class StreamChat {
|
|
|
1319
1321
|
* @returns {GetThreadAPIResponse} Returns the updated thread.
|
|
1320
1322
|
*/
|
|
1321
1323
|
partialUpdateThread(messageId: string, partialThreadObject: PartialThreadUpdate): Promise<GetThreadAPIResponse>;
|
|
1322
|
-
getUserAgent()
|
|
1324
|
+
getUserAgent: () => string;
|
|
1323
1325
|
/**
|
|
1324
1326
|
* @deprecated use sdkIdentifier instead
|
|
1325
1327
|
* @param userAgent
|
package/dist/types/events.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export declare const EVENT_MAP: {
|
|
|
60
60
|
'ai_indicator.update': boolean;
|
|
61
61
|
'ai_indicator.stop': boolean;
|
|
62
62
|
'ai_indicator.clear': boolean;
|
|
63
|
+
'message.read_locally': boolean;
|
|
63
64
|
'channels.queried': boolean;
|
|
64
65
|
'offline_reactions.queried': boolean;
|
|
65
66
|
'connection.changed': boolean;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1371,6 +1371,12 @@ export type StreamChatOptions = AxiosRequestConfig & {
|
|
|
1371
1371
|
*/
|
|
1372
1372
|
disableCache?: boolean;
|
|
1373
1373
|
enableInsights?: boolean;
|
|
1374
|
+
/**
|
|
1375
|
+
* When true, maintains a client-local unread count on channels that have read events disabled
|
|
1376
|
+
* (e.g. livestreams). The count increments on incoming messages and is reset via
|
|
1377
|
+
* `channel.markReadLocally()`. It is never sent to the backend, but is persisted to the offline DB.
|
|
1378
|
+
*/
|
|
1379
|
+
isLocalUnreadCountEnabled?: boolean;
|
|
1374
1380
|
/** experimental feature, please contact support if you want this feature enabled for you */
|
|
1375
1381
|
enableWSFallback?: boolean;
|
|
1376
1382
|
logger?: Logger;
|
|
@@ -3389,6 +3395,16 @@ export type DeviceIdentifier = {
|
|
|
3389
3395
|
os: string;
|
|
3390
3396
|
model?: string;
|
|
3391
3397
|
};
|
|
3398
|
+
/**
|
|
3399
|
+
* An identifier containing information about the downstream application integrating
|
|
3400
|
+
* stream-chat, if available. `name` is reported as `app` and `version` as `app_version`
|
|
3401
|
+
* in the user agent. Distinct from the SDK ({@link SdkIdentifier}) and device
|
|
3402
|
+
* ({@link DeviceIdentifier}) identifiers.
|
|
3403
|
+
*/
|
|
3404
|
+
export type AppIdentifier = {
|
|
3405
|
+
name: string;
|
|
3406
|
+
version?: string;
|
|
3407
|
+
};
|
|
3392
3408
|
export type DraftResponse = {
|
|
3393
3409
|
channel_cid: string;
|
|
3394
3410
|
created_at: string;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -20,6 +20,19 @@ export declare const chatCodes: {
|
|
|
20
20
|
};
|
|
21
21
|
export declare function isOwnUser(user?: OwnUserResponse | UserResponse): user is OwnUserResponse;
|
|
22
22
|
export declare function isOwnUserBaseProperty(property: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* channelHasReadEvents - Whether read events are enabled for the current user on a channel.
|
|
25
|
+
*/
|
|
26
|
+
export declare const channelHasReadEvents: (channel?: Channel) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* channelTracksReadLocally - Whether a channel maintains a client local unread count.
|
|
29
|
+
*/
|
|
30
|
+
export declare const channelTracksReadLocally: (channel?: Channel) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* userHasReadReceipts - Whether the current user allows read receipts, per their privacy settings.
|
|
33
|
+
* Read receipts are treated as enabled unless the user has explicitly disabled them.
|
|
34
|
+
*/
|
|
35
|
+
export declare const userHasReadReceipts: (client: StreamChat) => boolean;
|
|
23
36
|
export declare function addFileToFormData(uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string): FormData;
|
|
24
37
|
export declare function normalizeQuerySort<T extends Record<string, AscDesc | undefined>>(sort: T | T[]): {
|
|
25
38
|
direction: AscDesc;
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { CooldownTimer } from './CooldownTimer';
|
|
|
4
4
|
import { MessageComposer } from './messageComposer';
|
|
5
5
|
import { MessageReceiptsTracker } from './messageDelivery';
|
|
6
6
|
import {
|
|
7
|
+
channelHasReadEvents,
|
|
7
8
|
generateChannelTempCid,
|
|
8
9
|
logChatPromiseExecution,
|
|
9
10
|
messageSetPagination,
|
|
@@ -1247,6 +1248,36 @@ export class Channel {
|
|
|
1247
1248
|
});
|
|
1248
1249
|
}
|
|
1249
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* markReadLocally - Resets this user's unread count locally, without any backend call. Intended for
|
|
1253
|
+
* channels that have read events disabled (e.g. livestreams) when the client is created with the
|
|
1254
|
+
* `isLocalUnreadCountEnabled` option. Dispatches a dedicated, client-only `message.read_locally` event
|
|
1255
|
+
* that runs through the same `_handleChannelEvent` read logic as a real `message.read` (minus the
|
|
1256
|
+
* delivery-report network sync), so the read-state update lives in one place. When offline support
|
|
1257
|
+
* is enabled, the offline DB persists the reset for read-events-disabled channels, so the local
|
|
1258
|
+
* count stays consistent across app restarts.
|
|
1259
|
+
*
|
|
1260
|
+
* @return {Event | undefined} The dispatched `message.read_locally` event, or `undefined` if there is no connected user.
|
|
1261
|
+
*/
|
|
1262
|
+
markReadLocally() {
|
|
1263
|
+
const client = this.getClient();
|
|
1264
|
+
if (!client.userID) return;
|
|
1265
|
+
|
|
1266
|
+
const event: Event = {
|
|
1267
|
+
channel_id: this.id,
|
|
1268
|
+
channel_type: this.type,
|
|
1269
|
+
cid: this.cid,
|
|
1270
|
+
created_at: new Date().toISOString(),
|
|
1271
|
+
last_read_message_id: this.lastMessage()?.id,
|
|
1272
|
+
team: this.data?.team,
|
|
1273
|
+
type: 'message.read_locally',
|
|
1274
|
+
user: client.user,
|
|
1275
|
+
};
|
|
1276
|
+
client.dispatchEvent(event);
|
|
1277
|
+
|
|
1278
|
+
return event;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1250
1281
|
/**
|
|
1251
1282
|
* clean - Cleans the channel state and fires stop typing if needed
|
|
1252
1283
|
*/
|
|
@@ -1429,10 +1460,11 @@ export class Channel {
|
|
|
1429
1460
|
if (message.user?.id && this.getClient().userMuteStatus(message.user.id))
|
|
1430
1461
|
return false;
|
|
1431
1462
|
|
|
1432
|
-
// Return false if channel doesn't allow read events
|
|
1463
|
+
// Return false if channel doesn't allow read events, unless the client opted into a local
|
|
1464
|
+
// unread count (e.g. livestreams where read events are disabled). See `isLocalUnreadCountEnabled`.
|
|
1433
1465
|
if (
|
|
1434
|
-
|
|
1435
|
-
!this
|
|
1466
|
+
!this.getClient().options.isLocalUnreadCountEnabled &&
|
|
1467
|
+
!channelHasReadEvents(this)
|
|
1436
1468
|
) {
|
|
1437
1469
|
return false;
|
|
1438
1470
|
}
|
|
@@ -1965,6 +1997,11 @@ export class Channel {
|
|
|
1965
1997
|
delete channelState.typing[event.user.id];
|
|
1966
1998
|
}
|
|
1967
1999
|
break;
|
|
2000
|
+
// `message.read_locally` is the client-only event dispatched by `markReadLocally()` when read
|
|
2001
|
+
// events are disabled (e.g. livestreams with `isLocalUnreadCountEnabled`). It reuses the exact
|
|
2002
|
+
// `message.read` state logic so the read-state update lives in one place — only the
|
|
2003
|
+
// delivery-report network sync below is skipped for it.
|
|
2004
|
+
case 'message.read_locally':
|
|
1968
2005
|
case 'message.read':
|
|
1969
2006
|
if (event.user?.id && event.created_at) {
|
|
1970
2007
|
const previousReadState = channelState.read[event.user.id];
|
|
@@ -1987,7 +2024,11 @@ export class Channel {
|
|
|
1987
2024
|
|
|
1988
2025
|
if (isOwnEvent) {
|
|
1989
2026
|
channelState.unreadCount = 0;
|
|
1990
|
-
|
|
2027
|
+
// Delivery reporting buffers a `markChannelsDelivered` network request; the local read
|
|
2028
|
+
// must not hit the backend, so only sync for the real server `message.read`.
|
|
2029
|
+
if (event.type === 'message.read') {
|
|
2030
|
+
client.syncDeliveredCandidates([this]);
|
|
2031
|
+
}
|
|
1991
2032
|
}
|
|
1992
2033
|
}
|
|
1993
2034
|
break;
|
package/src/client.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
AddUserGroupMembersResponse,
|
|
47
47
|
APIErrorResponse,
|
|
48
48
|
APIResponse,
|
|
49
|
+
AppIdentifier,
|
|
49
50
|
AppSettings,
|
|
50
51
|
AppSettingsAPIResponse,
|
|
51
52
|
BannedUsersFilters,
|
|
@@ -394,6 +395,8 @@ export class StreamChat {
|
|
|
394
395
|
defaultWSTimeout: number;
|
|
395
396
|
sdkIdentifier?: SdkIdentifier;
|
|
396
397
|
deviceIdentifier?: DeviceIdentifier;
|
|
398
|
+
appIdentifier?: AppIdentifier;
|
|
399
|
+
private cachedUserAgent?: string;
|
|
397
400
|
readonly messageComposerCache: FixedSizeQueueCache<string, MessageComposer>;
|
|
398
401
|
private nextRequestAbortController: AbortController | null = null;
|
|
399
402
|
/**
|
|
@@ -467,6 +470,7 @@ export class StreamChat {
|
|
|
467
470
|
warmUp: false,
|
|
468
471
|
recoverStateOnReconnect: true,
|
|
469
472
|
disableCache: false,
|
|
473
|
+
isLocalUnreadCountEnabled: false,
|
|
470
474
|
wsUrlParams: new URLSearchParams({}),
|
|
471
475
|
...inputOptions,
|
|
472
476
|
};
|
|
@@ -1641,6 +1645,19 @@ export class StreamChat {
|
|
|
1641
1645
|
});
|
|
1642
1646
|
}
|
|
1643
1647
|
|
|
1648
|
+
// Current user removed from a channel: evict it like channel.deleted so
|
|
1649
|
+
// recoverState won't re-watch it and no further events reach it (#2599).
|
|
1650
|
+
// Type-agnostic. We skip deleteAllChannelReference (unlike deletion) since
|
|
1651
|
+
// the channel still exists for its remaining members.
|
|
1652
|
+
if (event.type === 'notification.removed_from_channel' && event.cid) {
|
|
1653
|
+
const { cid } = event;
|
|
1654
|
+
this.activeChannels[cid]?._disconnect();
|
|
1655
|
+
|
|
1656
|
+
postListenerCallbacks.push(() => {
|
|
1657
|
+
delete this.activeChannels[cid];
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1644
1661
|
return postListenerCallbacks;
|
|
1645
1662
|
}
|
|
1646
1663
|
|
|
@@ -3709,40 +3726,42 @@ export class StreamChat {
|
|
|
3709
3726
|
);
|
|
3710
3727
|
}
|
|
3711
3728
|
|
|
3712
|
-
getUserAgent() {
|
|
3729
|
+
getUserAgent = (): string => {
|
|
3730
|
+
// An explicit override (deprecated `setUserAgent`) always wins and is never cached.
|
|
3713
3731
|
if (this.userAgent) {
|
|
3714
3732
|
return this.userAgent;
|
|
3715
3733
|
}
|
|
3716
3734
|
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3735
|
+
// Computed once, then memoized for the client's lifetime - inputs read on
|
|
3736
|
+
// the first call (sdk/device identifiers, build-time env) are not re-read.
|
|
3737
|
+
if (!this.cachedUserAgent) {
|
|
3738
|
+
const version = process.env.PKG_VERSION;
|
|
3739
|
+
const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
|
|
3740
|
+
const { name: appName, version: appVersion } = this.appIdentifier ?? {};
|
|
3741
|
+
const { os, model: deviceModel } = this.deviceIdentifier ?? {};
|
|
3742
|
+
|
|
3743
|
+
const head = sdkName
|
|
3744
|
+
? `stream-chat-${sdkName}-v${sdkVersion}-llc-v${version}`
|
|
3745
|
+
: `stream-chat-js-v${version}-${this.node ? 'node' : 'browser'}`;
|
|
3746
|
+
|
|
3747
|
+
this.cachedUserAgent = [
|
|
3748
|
+
head,
|
|
3749
|
+
// reports the host app, the device OS, the device model, and the picked
|
|
3750
|
+
// exports bundle, each only when a value is present
|
|
3751
|
+
...Object.entries({
|
|
3752
|
+
app: appName,
|
|
3753
|
+
app_version: appVersion,
|
|
3754
|
+
os,
|
|
3755
|
+
device_model: deviceModel,
|
|
3756
|
+
client_bundle: process.env.CLIENT_BUNDLE,
|
|
3757
|
+
})
|
|
3758
|
+
.filter(([, value]) => value && value.length > 0)
|
|
3759
|
+
.map(([key, value]) => `${key}=${value}`),
|
|
3760
|
+
].join('|');
|
|
3725
3761
|
}
|
|
3726
3762
|
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
return (
|
|
3730
|
-
[
|
|
3731
|
-
// reports the device OS, if provided
|
|
3732
|
-
['os', os],
|
|
3733
|
-
// reports the device model, if provided
|
|
3734
|
-
['device_model', model],
|
|
3735
|
-
// reports which bundle is being picked from the exports
|
|
3736
|
-
['client_bundle', clientBundle],
|
|
3737
|
-
] as const
|
|
3738
|
-
).reduce(
|
|
3739
|
-
(withArguments, [key, value]) =>
|
|
3740
|
-
value && value.length > 0
|
|
3741
|
-
? withArguments.concat(`|${key}=${value}`)
|
|
3742
|
-
: withArguments,
|
|
3743
|
-
userAgentString,
|
|
3744
|
-
);
|
|
3745
|
-
}
|
|
3763
|
+
return this.cachedUserAgent;
|
|
3764
|
+
};
|
|
3746
3765
|
|
|
3747
3766
|
/**
|
|
3748
3767
|
* @deprecated use sdkIdentifier instead
|
package/src/events.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
MarkReadOptions,
|
|
11
11
|
} from '../types';
|
|
12
12
|
import { type APIErrorResponse } from '../types';
|
|
13
|
-
import { throttle } from '../utils';
|
|
13
|
+
import { throttle, userHasReadReceipts } from '../utils';
|
|
14
14
|
import { isAPIError, isErrorRetryable } from '../errors';
|
|
15
15
|
|
|
16
16
|
const MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD = 100 as const;
|
|
@@ -279,6 +279,8 @@ export class MessageDeliveryReporter {
|
|
|
279
279
|
* @param options
|
|
280
280
|
*/
|
|
281
281
|
public markRead = async (collection: Channel | Thread, options?: MarkReadOptions) => {
|
|
282
|
+
if (!userHasReadReceipts(this.client)) return null;
|
|
283
|
+
|
|
282
284
|
let result: EventAPIResponse | null = null;
|
|
283
285
|
if (isChannel(collection)) {
|
|
284
286
|
result = await collection.markAsReadRequest(options);
|
|
@@ -18,7 +18,12 @@ import type { StreamChat } from '../client';
|
|
|
18
18
|
import type { AxiosError } from 'axios';
|
|
19
19
|
import { OfflineDBSyncManager } from './offline_sync_manager';
|
|
20
20
|
import { StateStore } from '../store';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
channelHasReadEvents,
|
|
23
|
+
channelTracksReadLocally,
|
|
24
|
+
localMessageToNewMessagePayload,
|
|
25
|
+
runDetached,
|
|
26
|
+
} from '../utils';
|
|
22
27
|
import { isMessageUpdateReplayable } from './util';
|
|
23
28
|
|
|
24
29
|
/**
|
|
@@ -609,7 +614,13 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
609
614
|
if (cid && client.user && client.user.id !== user?.id) {
|
|
610
615
|
const userId = client.user.id;
|
|
611
616
|
const channel = client.activeChannels[cid];
|
|
612
|
-
|
|
617
|
+
// Persist the current user's read state for channels that track reads server-side. When the
|
|
618
|
+
// client opted into a local unread count, also persist it for read-events-disabled channels
|
|
619
|
+
// (e.g. livestreams) so the client-local count survives a cold start. The server never sends
|
|
620
|
+
// a read for those, so state.read[userId] may be absent here; fall back accordingly and rely
|
|
621
|
+
// on countUnread() (the aggregate the local count maintains) for the value.
|
|
622
|
+
const tracksReadLocally = channelTracksReadLocally(channel);
|
|
623
|
+
if (channel && (channelHasReadEvents(channel) || tracksReadLocally)) {
|
|
613
624
|
const ownReads = channel.state.read[userId];
|
|
614
625
|
const unreadCount = channel.countUnread();
|
|
615
626
|
const upsertReadsQueries = await this.upsertReads({
|
|
@@ -617,8 +628,8 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
617
628
|
execute: false,
|
|
618
629
|
reads: [
|
|
619
630
|
{
|
|
620
|
-
last_read: ownReads
|
|
621
|
-
last_read_message_id: ownReads
|
|
631
|
+
last_read: (ownReads?.last_read ?? new Date(0)).toISOString() as string,
|
|
632
|
+
last_read_message_id: ownReads?.last_read_message_id,
|
|
622
633
|
unread_messages: unreadCount,
|
|
623
634
|
user: client.user,
|
|
624
635
|
},
|
|
@@ -867,32 +878,38 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
867
878
|
execute: false,
|
|
868
879
|
});
|
|
869
880
|
|
|
881
|
+
let finalQueries = [...truncateQueries];
|
|
882
|
+
|
|
883
|
+
// Persist read state for channels that track reads server-side. When the client opted into a
|
|
884
|
+
// local unread count, also persist it for read-events-disabled channels (e.g. livestreams) so
|
|
885
|
+
// the client-local count survives a cold start. state.read[userId] may be absent for those, so
|
|
886
|
+
// fall back accordingly.
|
|
870
887
|
const userId = ownUser.id;
|
|
871
888
|
const activeChannel = this.client.activeChannels[cid];
|
|
872
|
-
const
|
|
889
|
+
const tracksReadLocally = channelTracksReadLocally(activeChannel);
|
|
890
|
+
if (activeChannel && (channelHasReadEvents(activeChannel) || tracksReadLocally)) {
|
|
891
|
+
const ownReads = activeChannel.state.read[userId];
|
|
873
892
|
|
|
874
|
-
|
|
893
|
+
let unreadCount = 0;
|
|
894
|
+
if (truncated_at) {
|
|
895
|
+
unreadCount = activeChannel.countUnread(new Date(truncated_at));
|
|
896
|
+
}
|
|
875
897
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
898
|
+
const upsertReadQueries = await this.upsertReads({
|
|
899
|
+
cid,
|
|
900
|
+
execute: false,
|
|
901
|
+
reads: [
|
|
902
|
+
{
|
|
903
|
+
last_read: (ownReads?.last_read ?? new Date(0)).toString() as string,
|
|
904
|
+
last_read_message_id: ownReads?.last_read_message_id,
|
|
905
|
+
unread_messages: unreadCount,
|
|
906
|
+
user: ownUser,
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
});
|
|
910
|
+
finalQueries = [...finalQueries, ...upsertReadQueries];
|
|
879
911
|
}
|
|
880
912
|
|
|
881
|
-
const upsertReadQueries = await this.upsertReads({
|
|
882
|
-
cid,
|
|
883
|
-
execute: false,
|
|
884
|
-
reads: [
|
|
885
|
-
{
|
|
886
|
-
last_read: ownReads.last_read.toString() as string,
|
|
887
|
-
last_read_message_id: ownReads.last_read_message_id,
|
|
888
|
-
unread_messages: unreadCount,
|
|
889
|
-
user: ownUser,
|
|
890
|
-
},
|
|
891
|
-
],
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
const finalQueries = [...truncateQueries, ...upsertReadQueries];
|
|
895
|
-
|
|
896
913
|
if (execute) {
|
|
897
914
|
await this.executeSqlBatch(finalQueries);
|
|
898
915
|
}
|
|
@@ -1021,6 +1038,18 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
1021
1038
|
return this.handleRead({ event, unreadMessages: 0, execute });
|
|
1022
1039
|
}
|
|
1023
1040
|
|
|
1041
|
+
// `message.read_locally` is the client-only reset dispatched by `Channel.markReadLocally()`. Only
|
|
1042
|
+
// persist it for read-events-disabled channels with the local unread count opted in (the only
|
|
1043
|
+
// situation it is ever dispatched), so it can never touch channels that track reads server-side.
|
|
1044
|
+
if (type === 'message.read_locally') {
|
|
1045
|
+
const localChannel = event.cid ? this.client.activeChannels[event.cid] : undefined;
|
|
1046
|
+
// channelTracksReadLocally returns false when localChannel is undefined, so no extra guard.
|
|
1047
|
+
if (channelTracksReadLocally(localChannel)) {
|
|
1048
|
+
return this.handleRead({ event, unreadMessages: 0, execute });
|
|
1049
|
+
}
|
|
1050
|
+
return [];
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1024
1053
|
if (type === 'notification.mark_unread') {
|
|
1025
1054
|
return this.handleRead({ event, execute });
|
|
1026
1055
|
}
|
package/src/types.ts
CHANGED
|
@@ -1591,6 +1591,12 @@ export type StreamChatOptions = AxiosRequestConfig & {
|
|
|
1591
1591
|
*/
|
|
1592
1592
|
disableCache?: boolean;
|
|
1593
1593
|
enableInsights?: boolean;
|
|
1594
|
+
/**
|
|
1595
|
+
* When true, maintains a client-local unread count on channels that have read events disabled
|
|
1596
|
+
* (e.g. livestreams). The count increments on incoming messages and is reset via
|
|
1597
|
+
* `channel.markReadLocally()`. It is never sent to the backend, but is persisted to the offline DB.
|
|
1598
|
+
*/
|
|
1599
|
+
isLocalUnreadCountEnabled?: boolean;
|
|
1594
1600
|
/** experimental feature, please contact support if you want this feature enabled for you */
|
|
1595
1601
|
enableWSFallback?: boolean;
|
|
1596
1602
|
logger?: Logger;
|
|
@@ -4537,6 +4543,14 @@ export type SdkIdentifier = {
|
|
|
4537
4543
|
*/
|
|
4538
4544
|
export type DeviceIdentifier = { os: string; model?: string };
|
|
4539
4545
|
|
|
4546
|
+
/**
|
|
4547
|
+
* An identifier containing information about the downstream application integrating
|
|
4548
|
+
* stream-chat, if available. `name` is reported as `app` and `version` as `app_version`
|
|
4549
|
+
* in the user agent. Distinct from the SDK ({@link SdkIdentifier}) and device
|
|
4550
|
+
* ({@link DeviceIdentifier}) identifiers.
|
|
4551
|
+
*/
|
|
4552
|
+
export type AppIdentifier = { name: string; version?: string };
|
|
4553
|
+
|
|
4540
4554
|
export type DraftResponse = {
|
|
4541
4555
|
channel_cid: string;
|
|
4542
4556
|
created_at: string;
|
package/src/utils.ts
CHANGED
|
@@ -110,6 +110,28 @@ export function isOwnUserBaseProperty(property: string) {
|
|
|
110
110
|
return ownUserBaseProperties[property as keyof OwnUserBase];
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* channelHasReadEvents - Whether read events are enabled for the current user on a channel.
|
|
115
|
+
*/
|
|
116
|
+
export const channelHasReadEvents = (channel?: Channel) => {
|
|
117
|
+
const ownCapabilities = channel?.data?.own_capabilities;
|
|
118
|
+
return !(Array.isArray(ownCapabilities) && !ownCapabilities.includes('read-events'));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* channelTracksReadLocally - Whether a channel maintains a client local unread count.
|
|
123
|
+
*/
|
|
124
|
+
export const channelTracksReadLocally = (channel?: Channel) =>
|
|
125
|
+
!channelHasReadEvents(channel) &&
|
|
126
|
+
!!channel?.getClient().options.isLocalUnreadCountEnabled;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* userHasReadReceipts - Whether the current user allows read receipts, per their privacy settings.
|
|
130
|
+
* Read receipts are treated as enabled unless the user has explicitly disabled them.
|
|
131
|
+
*/
|
|
132
|
+
export const userHasReadReceipts = (client: StreamChat) =>
|
|
133
|
+
client.user?.privacy_settings?.read_receipts?.enabled ?? true;
|
|
134
|
+
|
|
113
135
|
export function addFileToFormData(
|
|
114
136
|
uri: string | NodeJS.ReadableStream | Buffer | File,
|
|
115
137
|
name?: string,
|