stream-chat 9.52.1 → 9.54.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 +388 -90
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +388 -90
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +388 -90
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/channel_batch_updater.d.ts +9 -2
- package/dist/types/client.d.ts +19 -6
- package/dist/types/poll.d.ts +22 -4
- package/dist/types/poll_optimistic_votes.d.ts +63 -0
- package/dist/types/types.d.ts +43 -3
- package/dist/types/utils.d.ts +1 -3
- package/package.json +1 -1
- package/src/channel_batch_updater.ts +28 -2
- package/src/client.ts +84 -10
- package/src/messageComposer/middleware/messageComposer/linkPreviews.ts +20 -10
- package/src/poll.ts +86 -60
- package/src/poll_optimistic_votes.ts +321 -0
- package/src/types.ts +51 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StreamChat } from './client';
|
|
2
|
-
import type { APIResponse, BatchChannelDataUpdate, NewMemberPayload, UpdateChannelsBatchFilters, UpdateChannelsBatchResponse } from './types';
|
|
2
|
+
import type { APIResponse, BatchChannelDataUpdate, ChannelBatchDataUpdateOptions, NewMemberPayload, UpdateChannelsBatchFilters, UpdateChannelsBatchResponse } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* ChannelBatchUpdater - A class that provides convenience methods for batch channel operations
|
|
5
5
|
*/
|
|
@@ -85,9 +85,16 @@ export declare class ChannelBatchUpdater {
|
|
|
85
85
|
/**
|
|
86
86
|
* updateData - Update data on channels matching the filter
|
|
87
87
|
*
|
|
88
|
+
* `data.custom` replaces the channel's whole custom object. To patch
|
|
89
|
+
* individual custom keys, pass an options object with `custom_set` or
|
|
90
|
+
* `custom_unset`. The client sends those fields at the request root rather
|
|
91
|
+
* than inside `data`. The options object can also include `data` to update
|
|
92
|
+
* channel fields in the same request.
|
|
93
|
+
*
|
|
88
94
|
* @param {UpdateChannelsBatchFilters} filter Filter to select channels
|
|
89
|
-
* @param {BatchChannelDataUpdate}
|
|
95
|
+
* @param {BatchChannelDataUpdate | ChannelBatchDataUpdateOptions} update Data or update options
|
|
90
96
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
91
97
|
*/
|
|
92
98
|
updateData(filter: UpdateChannelsBatchFilters, data: BatchChannelDataUpdate): Promise<APIResponse & UpdateChannelsBatchResponse>;
|
|
99
|
+
updateData(filter: UpdateChannelsBatchFilters, options: ChannelBatchDataUpdateOptions): Promise<APIResponse & UpdateChannelsBatchResponse>;
|
|
93
100
|
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -105,6 +105,10 @@ export declare class StreamChat {
|
|
|
105
105
|
wsConnection: StableWSConnection | null;
|
|
106
106
|
wsFallback?: WSConnectionFallback;
|
|
107
107
|
wsPromise: ConnectAPIResponse | null;
|
|
108
|
+
private _wsPromiseSettled;
|
|
109
|
+
private _wsConnectId;
|
|
110
|
+
private _resolveWsPromise?;
|
|
111
|
+
private _rejectWsPromise?;
|
|
108
112
|
consecutiveFailures: number;
|
|
109
113
|
insightMetrics: InsightMetrics;
|
|
110
114
|
defaultWSTimeoutWithFallback: number;
|
|
@@ -224,6 +228,15 @@ export declare class StreamChat {
|
|
|
224
228
|
* Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
|
|
225
229
|
*/
|
|
226
230
|
openConnection: () => ConnectAPIResponse | undefined;
|
|
231
|
+
/**
|
|
232
|
+
* Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
|
|
233
|
+
*/
|
|
234
|
+
private _rejectPendingWsPromise;
|
|
235
|
+
/**
|
|
236
|
+
* Keep a single pending `wsPromise` across close/reopen so callers that already
|
|
237
|
+
* `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
|
|
238
|
+
*/
|
|
239
|
+
private _bindWsPromise;
|
|
227
240
|
/**
|
|
228
241
|
* @deprecated Please use client.openConnction instead.
|
|
229
242
|
* @private
|
|
@@ -1140,9 +1153,7 @@ export declare class StreamChat {
|
|
|
1140
1153
|
created_at?: string;
|
|
1141
1154
|
deleted_at?: string;
|
|
1142
1155
|
deleted_reply_count?: number;
|
|
1143
|
-
i18n?: import("./types").
|
|
1144
|
-
language: import("./types").TranslationLanguages;
|
|
1145
|
-
};
|
|
1156
|
+
i18n?: import("./types").TranslatedField;
|
|
1146
1157
|
latest_reactions?: ReactionResponse[];
|
|
1147
1158
|
member?: import("./types").ChannelMemberResponse;
|
|
1148
1159
|
mentioned_users?: UserResponse[];
|
|
@@ -1878,9 +1889,7 @@ export declare class StreamChat {
|
|
|
1878
1889
|
created_at?: string;
|
|
1879
1890
|
deleted_at?: string;
|
|
1880
1891
|
deleted_reply_count?: number;
|
|
1881
|
-
i18n?: import("./types").
|
|
1882
|
-
language: import("./types").TranslationLanguages;
|
|
1883
|
-
};
|
|
1892
|
+
i18n?: import("./types").TranslatedField;
|
|
1884
1893
|
latest_reactions?: ReactionResponse[];
|
|
1885
1894
|
member?: import("./types").ChannelMemberResponse;
|
|
1886
1895
|
mentioned_users?: UserResponse[];
|
|
@@ -2183,6 +2192,10 @@ export declare class StreamChat {
|
|
|
2183
2192
|
/**
|
|
2184
2193
|
* Update Channels Batch
|
|
2185
2194
|
*
|
|
2195
|
+
* For the `updateData` operation, `data.custom` replaces the channel's whole
|
|
2196
|
+
* custom object, while the root-level `custom_set` / `custom_unset` patch
|
|
2197
|
+
* individual keys and leave the rest untouched.
|
|
2198
|
+
*
|
|
2186
2199
|
* @param {UpdateChannelsBatchOptions} payload for updating channels in batch
|
|
2187
2200
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
2188
2201
|
*/
|
package/dist/types/poll.d.ts
CHANGED
|
@@ -29,10 +29,13 @@ export declare class Poll {
|
|
|
29
29
|
readonly state: StateStore<PollState>;
|
|
30
30
|
id: string;
|
|
31
31
|
private client;
|
|
32
|
+
private optimisticVotes;
|
|
33
|
+
private closeConfirmed;
|
|
32
34
|
constructor({ client, poll }: PollInitOptions);
|
|
33
35
|
private getInitialStateFromPollResponse;
|
|
34
36
|
private upsertOfflineDb;
|
|
35
37
|
reinitializeState: (poll: PollInitOptions["poll"]) => void;
|
|
38
|
+
private updateFromVoteEvent;
|
|
36
39
|
get data(): PollState;
|
|
37
40
|
handlePollUpdated: (event: Event) => void;
|
|
38
41
|
handlePollClosed: (event: Event) => void;
|
|
@@ -42,15 +45,30 @@ export declare class Poll {
|
|
|
42
45
|
query: (id: string) => Promise<PollResponse>;
|
|
43
46
|
update: (data: Exclude<PollData, "id">) => Promise<import("./types").APIResponse & import("./types").UpdatePollAPIResponse>;
|
|
44
47
|
partialUpdate: (partialPollObject: PartialPollUpdate) => Promise<import("./types").APIResponse & import("./types").UpdatePollAPIResponse>;
|
|
45
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Closes the poll. The poll is marked as closed optimistically and the change is
|
|
50
|
+
* reverted if the request fails, unless the server confirmed the close meanwhile.
|
|
51
|
+
* Does nothing if the poll is already closed.
|
|
52
|
+
*/
|
|
53
|
+
close: () => Promise<(import("./types").APIResponse & import("./types").UpdatePollAPIResponse) | undefined>;
|
|
46
54
|
delete: () => Promise<import("./types").APIResponse>;
|
|
47
55
|
createOption: (option: PollOptionData) => Promise<import("./types").APIResponse & import("./types").CreatePollOptionAPIResponse>;
|
|
48
56
|
updateOption: (option: PollOptionData) => Promise<import("./types").APIResponse & import("./types").CreatePollOptionAPIResponse>;
|
|
49
57
|
deleteOption: (optionId: string) => Promise<import("./types").APIResponse>;
|
|
58
|
+
/**
|
|
59
|
+
* Casts a vote for an option. The vote is shown right away through a local placeholder vote,
|
|
60
|
+
* which the server's vote replaces once the WS events of all pending vote changes arrived.
|
|
61
|
+
* If the request fails, the change is reverted.
|
|
62
|
+
*/
|
|
50
63
|
castVote: (optionId: string, messageId: string) => Promise<(import("./types").APIResponse & import("./types").CastVoteAPIResponse) | undefined>;
|
|
51
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Removes a vote. The vote is removed right away and restored if the request fails. Vote
|
|
66
|
+
* requests are sent in order, so removing a vote whose cast is not confirmed yet uses the vote
|
|
67
|
+
* id from the cast's response; if that cast failed, there is nothing to remove.
|
|
68
|
+
*/
|
|
69
|
+
removeVote: (voteId: string, messageId: string) => Promise<(import("./types").APIResponse & {
|
|
52
70
|
vote: PollVote;
|
|
53
|
-
}>;
|
|
71
|
+
}) | undefined>;
|
|
54
72
|
addAnswer: (answerText: string, messageId: string) => Promise<import("./types").APIResponse & import("./types").CastVoteAPIResponse>;
|
|
55
73
|
removeAnswer: (answerId: string, messageId: string) => Promise<import("./types").APIResponse & {
|
|
56
74
|
vote: PollVote;
|
|
@@ -59,6 +77,6 @@ export declare class Poll {
|
|
|
59
77
|
queryOptionVotes: (params: PollOptionVotesQueryParams) => Promise<import("./types").APIResponse & import("./types").PollVotesAPIResponse>;
|
|
60
78
|
}
|
|
61
79
|
export declare function extractPollData(pollResponse: PollResponse): PollData;
|
|
62
|
-
export declare function mapPollStateToResponse(poll: Poll): PollResponse;
|
|
80
|
+
export declare function mapPollStateToResponse(poll: Poll, state?: PollState): PollResponse;
|
|
63
81
|
export declare function extractPollEnrichedData(pollResponse: PollResponse): Omit<PollEnrichData, 'own_votes' | 'latest_answers'>;
|
|
64
82
|
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { StreamChat } from './client';
|
|
2
|
+
import type { Poll, PollState } from './poll';
|
|
3
|
+
import type { PollResponse, PollVote } from './types';
|
|
4
|
+
type OptionId = string;
|
|
5
|
+
/**
|
|
6
|
+
* Optimistic updates for the current user's votes on a poll. Vote changes are shown right away.
|
|
7
|
+
* While any of them is not confirmed by its WS event, vote events are only stored, as the server
|
|
8
|
+
* state they carry does not contain the pending changes yet. Once all are confirmed (a failed
|
|
9
|
+
* request counts as confirmed), the server's vote state is shown, which also reverts failed changes.
|
|
10
|
+
* Vote requests are sent one after another, so the server applies them in the order they were made.
|
|
11
|
+
* A lost WS event is recovered from after CATCH_UP_TIMEOUT_MS (see applySucceededChanges).
|
|
12
|
+
*/
|
|
13
|
+
export declare class OptimisticPollVotes {
|
|
14
|
+
private serverVoteState;
|
|
15
|
+
private pendingCount;
|
|
16
|
+
private succeededChanges;
|
|
17
|
+
private readonly requestTag;
|
|
18
|
+
private catchUpTimeout?;
|
|
19
|
+
private readonly client;
|
|
20
|
+
private readonly poll;
|
|
21
|
+
constructor({ client, poll }: {
|
|
22
|
+
client: StreamChat;
|
|
23
|
+
poll: Poll;
|
|
24
|
+
});
|
|
25
|
+
get serverOwnVotes(): Record<string, PollVote>;
|
|
26
|
+
withServerVotes: (pollState: PollState) => PollState;
|
|
27
|
+
/**
|
|
28
|
+
* Casts a vote for an option, shown right away through a placeholder vote. In polls with unique
|
|
29
|
+
* votes, it replaces the existing own votes.
|
|
30
|
+
*/
|
|
31
|
+
castVote: (optionId: OptionId, messageId: string) => Promise<(import("./types").APIResponse & import("./types").CastVoteAPIResponse) | undefined>;
|
|
32
|
+
removeVote: (voteId: string, messageId: string) => Promise<(import("./types").APIResponse & {
|
|
33
|
+
vote: PollVote;
|
|
34
|
+
}) | undefined>;
|
|
35
|
+
private send;
|
|
36
|
+
handleVoteEvent: (poll: PollResponse, { confirmsOwnVote, ownVotesByOptionId, }: {
|
|
37
|
+
confirmsOwnVote: boolean;
|
|
38
|
+
ownVotesByOptionId: Record<OptionId, PollVote>;
|
|
39
|
+
}) => Partial<PollState>;
|
|
40
|
+
reset: (pollState: PollState) => void;
|
|
41
|
+
private track;
|
|
42
|
+
private settleWithoutEvent;
|
|
43
|
+
private createPlaceholderVote;
|
|
44
|
+
/**
|
|
45
|
+
* The server id of a vote to remove, once everything sent before the removal has finished. Casts
|
|
46
|
+
* and removals of an option alternate, so the option's last succeeded change tells: a cast is the
|
|
47
|
+
* vote being removed (e.g. a placeholder), a removal means the cast in between failed. Without
|
|
48
|
+
* one, the server's own vote of the option is the vote being removed.
|
|
49
|
+
*/
|
|
50
|
+
private getServerVoteId;
|
|
51
|
+
private takeServerVoteState;
|
|
52
|
+
private scheduleCatchUp;
|
|
53
|
+
/**
|
|
54
|
+
* A WS event of an own vote change got lost, so the server's own votes are stale. All requests
|
|
55
|
+
* have finished (the timeout starts once nothing is left to send and a new request cancels it),
|
|
56
|
+
* so applying the succeeded changes in order gives the server's own votes (changes whose events
|
|
57
|
+
* did arrive end up the same). They are shown with the vote counts of the last WS event, which
|
|
58
|
+
* contain the other users' votes too.
|
|
59
|
+
*/
|
|
60
|
+
private applySucceededChanges;
|
|
61
|
+
}
|
|
62
|
+
export declare function getMaxVotedOptionIds(voteCountsByOption: PollResponse['vote_counts_by_option']): string[];
|
|
63
|
+
export {};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export type TranslateResponse = {
|
|
|
48
48
|
export type AppSettingsAPIResponse = APIResponse & {
|
|
49
49
|
app?: {
|
|
50
50
|
id?: string | number;
|
|
51
|
+
activity_metrics_config?: Record<string, number>;
|
|
51
52
|
allow_multi_user_devices?: boolean;
|
|
52
53
|
feed_audit_logs_enabled?: boolean;
|
|
53
54
|
moderation_onboarding_complete?: boolean | null;
|
|
@@ -91,8 +92,10 @@ export type AppSettingsAPIResponse = APIResponse & {
|
|
|
91
92
|
auto_translation_enabled?: boolean;
|
|
92
93
|
before_message_send_hook_url?: string;
|
|
93
94
|
before_message_send_hook_attempt_timeout_ms?: number;
|
|
95
|
+
before_message_send_hook_system_messages?: boolean;
|
|
94
96
|
campaign_enabled?: boolean;
|
|
95
97
|
cdn_expiration_seconds?: number;
|
|
98
|
+
chat_primary_use_case?: string;
|
|
96
99
|
custom_action_handler_url?: string;
|
|
97
100
|
datadog_info?: {
|
|
98
101
|
api_key: string;
|
|
@@ -117,13 +120,18 @@ export type AppSettingsAPIResponse = APIResponse & {
|
|
|
117
120
|
image_upload_config?: FileUploadConfig;
|
|
118
121
|
allowed_flag_reasons?: string[];
|
|
119
122
|
max_aggregated_activities_length?: number;
|
|
123
|
+
member_custom_on_mentioned_users_enabled?: boolean;
|
|
124
|
+
member_custom_on_messages_enabled?: boolean;
|
|
125
|
+
member_custom_on_typing_events_enabled?: boolean;
|
|
120
126
|
moderation_bulk_submit_action_enabled?: boolean;
|
|
121
127
|
moderation_dashboard_preferences?: Record<string, unknown> | null;
|
|
122
128
|
moderation_audio_call_moderation_enabled?: boolean;
|
|
123
129
|
moderation_enabled?: boolean;
|
|
124
130
|
moderation_flood_rules_enabled?: boolean;
|
|
131
|
+
moderation_keyframe_video_enabled?: boolean;
|
|
125
132
|
moderation_llm_configurability_enabled?: boolean;
|
|
126
133
|
moderation_multitenant_blocklist_enabled?: boolean;
|
|
134
|
+
moderation_s3_image_access_role_arn?: string;
|
|
127
135
|
moderation_video_call_moderation_enabled?: boolean;
|
|
128
136
|
moderation_webhook_url?: string;
|
|
129
137
|
multi_tenant_enabled?: boolean;
|
|
@@ -160,6 +168,7 @@ export type AppSettingsAPIResponse = APIResponse & {
|
|
|
160
168
|
use_hook_v2?: boolean;
|
|
161
169
|
user_response_time_enabled?: boolean;
|
|
162
170
|
user_search_disallowed_roles?: string[] | null;
|
|
171
|
+
video_primary_use_case?: string;
|
|
163
172
|
video_provider?: string;
|
|
164
173
|
webhook_events?: Array<string>;
|
|
165
174
|
webhook_url?: string;
|
|
@@ -659,9 +668,7 @@ export type MessageResponseBase = MessageBase & {
|
|
|
659
668
|
created_at?: string;
|
|
660
669
|
deleted_at?: string;
|
|
661
670
|
deleted_reply_count?: number;
|
|
662
|
-
i18n?:
|
|
663
|
-
language: TranslationLanguages;
|
|
664
|
-
};
|
|
671
|
+
i18n?: TranslatedField;
|
|
665
672
|
latest_reactions?: ReactionResponse[];
|
|
666
673
|
member?: ChannelMemberResponse;
|
|
667
674
|
mentioned_users?: UserResponse[];
|
|
@@ -2415,6 +2422,9 @@ export type TestSNSDataInput = {
|
|
|
2415
2422
|
export type TokenOrProvider = null | string | TokenProvider | undefined;
|
|
2416
2423
|
export type TokenProvider = () => Promise<string>;
|
|
2417
2424
|
export type TranslationLanguages = 'af' | 'am' | 'ar' | 'az' | 'bg' | 'bn' | 'bs' | 'cs' | 'da' | 'de' | 'el' | 'en' | 'es' | 'es-MX' | 'et' | 'fa' | 'fa-AF' | 'fi' | 'fr' | 'fr-CA' | 'ha' | 'he' | 'hi' | 'hr' | 'hu' | 'id' | 'it' | 'ja' | 'ka' | 'ko' | 'lt' | 'lv' | 'ms' | 'nl' | 'no' | 'pl' | 'ps' | 'pt' | 'ro' | 'ru' | 'sk' | 'sl' | 'so' | 'sq' | 'sr' | 'sv' | 'sw' | 'ta' | 'th' | 'tl' | 'tr' | 'uk' | 'ur' | 'vi' | 'zh' | 'zh-TW' | (string & {});
|
|
2425
|
+
export type TranslatedField = RequireAtLeastOne<Record<`${TranslationLanguages}_text`, string>> & {
|
|
2426
|
+
language: TranslationLanguages;
|
|
2427
|
+
};
|
|
2418
2428
|
export type TypingStartEvent = Event;
|
|
2419
2429
|
export type ReservedUpdatedMessageFields = keyof typeof RESERVED_UPDATED_MESSAGE_FIELDS;
|
|
2420
2430
|
export type UpdatedMessage = Omit<MessageResponse, ReservedUpdatedMessageFields | 'mentioned_groups'> & {
|
|
@@ -2687,7 +2697,9 @@ export type PollResponse = CustomPollData & PollEnrichData & {
|
|
|
2687
2697
|
allow_answers?: boolean;
|
|
2688
2698
|
allow_user_suggested_options?: boolean;
|
|
2689
2699
|
description?: string;
|
|
2700
|
+
description_i18n?: TranslatedField;
|
|
2690
2701
|
is_closed?: boolean;
|
|
2702
|
+
name_i18n?: TranslatedField;
|
|
2691
2703
|
voting_visibility?: VotingVisibility;
|
|
2692
2704
|
};
|
|
2693
2705
|
export type PollOption = {
|
|
@@ -2697,6 +2709,7 @@ export type PollOption = {
|
|
|
2697
2709
|
text: string;
|
|
2698
2710
|
updated_at: string;
|
|
2699
2711
|
vote_count: number;
|
|
2712
|
+
text_i18n?: TranslatedField;
|
|
2700
2713
|
votes?: PollVote[];
|
|
2701
2714
|
};
|
|
2702
2715
|
export declare enum VotingVisibility {
|
|
@@ -2760,6 +2773,7 @@ export type PollOptionResponse = CustomPollData & {
|
|
|
2760
2773
|
text: string;
|
|
2761
2774
|
updated_at: string;
|
|
2762
2775
|
vote_count: number;
|
|
2776
|
+
text_i18n?: TranslatedField;
|
|
2763
2777
|
votes?: PollVote[];
|
|
2764
2778
|
};
|
|
2765
2779
|
export type PollVote = {
|
|
@@ -2774,6 +2788,7 @@ export type PollVote = {
|
|
|
2774
2788
|
export type PollAnswer = Exclude<PollVote, 'option_id'> & {
|
|
2775
2789
|
answer_text: string;
|
|
2776
2790
|
is_answer: boolean;
|
|
2791
|
+
answer_text_i18n?: TranslatedField;
|
|
2777
2792
|
};
|
|
2778
2793
|
export type PollVotesAPIResponse = {
|
|
2779
2794
|
votes: (PollVote | PollAnswer)[];
|
|
@@ -3713,7 +3728,32 @@ export type UpdateChannelsBatchOptions = {
|
|
|
3713
3728
|
filter: UpdateChannelsBatchFilters;
|
|
3714
3729
|
members?: string[] | Array<NewMemberPayload>;
|
|
3715
3730
|
data?: BatchChannelDataUpdate;
|
|
3731
|
+
/**
|
|
3732
|
+
* `updateData` only. Merges these keys into each matched channel's existing
|
|
3733
|
+
* custom object, leaving every other custom key untouched — unlike
|
|
3734
|
+
* `data.custom`, which replaces the whole object. Keys are dot-paths, so
|
|
3735
|
+
* `a.b` sets key `b` inside object `a` (the parent object must already
|
|
3736
|
+
* exist). Cannot be combined with `data.custom`.
|
|
3737
|
+
*
|
|
3738
|
+
* Lives at the request root, not inside `data`.
|
|
3739
|
+
*/
|
|
3740
|
+
custom_set?: Record<string, unknown>;
|
|
3741
|
+
/**
|
|
3742
|
+
* `updateData` only. Deletes these keys from each matched channel's existing
|
|
3743
|
+
* custom object, leaving every other custom key untouched. Keys are
|
|
3744
|
+
* dot-paths; deleting a key that does not exist is a no-op. Cannot be
|
|
3745
|
+
* combined with `data.custom`.
|
|
3746
|
+
*
|
|
3747
|
+
* Lives at the request root, not inside `data`.
|
|
3748
|
+
*/
|
|
3749
|
+
custom_unset?: string[];
|
|
3716
3750
|
};
|
|
3751
|
+
/**
|
|
3752
|
+
* Options for {@link ChannelBatchUpdater.updateData}. `custom_set` and
|
|
3753
|
+
* `custom_unset` are sent at the request root, while `data` contains the
|
|
3754
|
+
* channel fields to update.
|
|
3755
|
+
*/
|
|
3756
|
+
export type ChannelBatchDataUpdateOptions = Pick<UpdateChannelsBatchOptions, 'data' | 'custom_set' | 'custom_unset'>;
|
|
3717
3757
|
export type UpdateChannelsBatchFilters = QueryFilters<{
|
|
3718
3758
|
cids?: RequireOnlyOne<Pick<QueryFilter<string>, '$in' | '$eq'>> | PrimitiveFilter<string[]>;
|
|
3719
3759
|
types?: RequireOnlyOne<Pick<QueryFilter<string>, '$in' | '$eq'>> | PrimitiveFilter<string[]>;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -134,9 +134,7 @@ export declare const toDeletedMessage: ({ message, deletedAt, hardDelete, }: {
|
|
|
134
134
|
[key: string]: number;
|
|
135
135
|
} | null | undefined;
|
|
136
136
|
reply_count?: number | undefined;
|
|
137
|
-
i18n?:
|
|
138
|
-
language: import("./types").TranslationLanguages;
|
|
139
|
-
}) | undefined;
|
|
137
|
+
i18n?: import("./types").TranslatedField | undefined;
|
|
140
138
|
html?: string | undefined;
|
|
141
139
|
user?: (UserResponse | null) | undefined;
|
|
142
140
|
id: string;
|
package/package.json
CHANGED
|
@@ -2,11 +2,17 @@ import type { StreamChat } from './client';
|
|
|
2
2
|
import type {
|
|
3
3
|
APIResponse,
|
|
4
4
|
BatchChannelDataUpdate,
|
|
5
|
+
ChannelBatchDataUpdateOptions,
|
|
5
6
|
NewMemberPayload,
|
|
6
7
|
UpdateChannelsBatchFilters,
|
|
7
8
|
UpdateChannelsBatchResponse,
|
|
8
9
|
} from './types';
|
|
9
10
|
|
|
11
|
+
const isChannelBatchDataUpdateOptions = (
|
|
12
|
+
update: BatchChannelDataUpdate | ChannelBatchDataUpdateOptions,
|
|
13
|
+
): update is ChannelBatchDataUpdateOptions =>
|
|
14
|
+
'data' in update || 'custom_set' in update || 'custom_unset' in update;
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* ChannelBatchUpdater - A class that provides convenience methods for batch channel operations
|
|
12
18
|
*/
|
|
@@ -194,18 +200,38 @@ export class ChannelBatchUpdater {
|
|
|
194
200
|
/**
|
|
195
201
|
* updateData - Update data on channels matching the filter
|
|
196
202
|
*
|
|
203
|
+
* `data.custom` replaces the channel's whole custom object. To patch
|
|
204
|
+
* individual custom keys, pass an options object with `custom_set` or
|
|
205
|
+
* `custom_unset`. The client sends those fields at the request root rather
|
|
206
|
+
* than inside `data`. The options object can also include `data` to update
|
|
207
|
+
* channel fields in the same request.
|
|
208
|
+
*
|
|
197
209
|
* @param {UpdateChannelsBatchFilters} filter Filter to select channels
|
|
198
|
-
* @param {BatchChannelDataUpdate}
|
|
210
|
+
* @param {BatchChannelDataUpdate | ChannelBatchDataUpdateOptions} update Data or update options
|
|
199
211
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
200
212
|
*/
|
|
201
213
|
async updateData(
|
|
202
214
|
filter: UpdateChannelsBatchFilters,
|
|
203
215
|
data: BatchChannelDataUpdate,
|
|
216
|
+
): Promise<APIResponse & UpdateChannelsBatchResponse>;
|
|
217
|
+
async updateData(
|
|
218
|
+
filter: UpdateChannelsBatchFilters,
|
|
219
|
+
options: ChannelBatchDataUpdateOptions,
|
|
220
|
+
): Promise<APIResponse & UpdateChannelsBatchResponse>;
|
|
221
|
+
async updateData(
|
|
222
|
+
filter: UpdateChannelsBatchFilters,
|
|
223
|
+
update: BatchChannelDataUpdate | ChannelBatchDataUpdateOptions,
|
|
204
224
|
): Promise<APIResponse & UpdateChannelsBatchResponse> {
|
|
225
|
+
const { data, custom_set, custom_unset } = isChannelBatchDataUpdateOptions(update)
|
|
226
|
+
? update
|
|
227
|
+
: { data: update };
|
|
228
|
+
|
|
205
229
|
return await this.client.updateChannelsBatch({
|
|
206
230
|
operation: 'updateData',
|
|
207
231
|
filter,
|
|
208
|
-
data,
|
|
232
|
+
...(data && { data }),
|
|
233
|
+
...(custom_set && { custom_set }),
|
|
234
|
+
...(custom_unset && { custom_unset }),
|
|
209
235
|
});
|
|
210
236
|
}
|
|
211
237
|
}
|
package/src/client.ts
CHANGED
|
@@ -393,6 +393,10 @@ export class StreamChat {
|
|
|
393
393
|
wsConnection: StableWSConnection | null;
|
|
394
394
|
wsFallback?: WSConnectionFallback;
|
|
395
395
|
wsPromise: ConnectAPIResponse | null;
|
|
396
|
+
private _wsPromiseSettled = true;
|
|
397
|
+
private _wsConnectId = 0;
|
|
398
|
+
private _resolveWsPromise?: (value: Awaited<ConnectAPIResponse>) => void;
|
|
399
|
+
private _rejectWsPromise?: (reason?: unknown) => void;
|
|
396
400
|
consecutiveFailures: number;
|
|
397
401
|
insightMetrics: InsightMetrics;
|
|
398
402
|
defaultWSTimeoutWithFallback: number;
|
|
@@ -713,18 +717,25 @@ export class StreamChat {
|
|
|
713
717
|
|
|
714
718
|
const wsPromise = this.openConnection();
|
|
715
719
|
|
|
716
|
-
|
|
720
|
+
const setUserPromise = Promise.all([setTokenPromise, wsPromise]).then(
|
|
717
721
|
(result) => result[1], // We only return connection promise;
|
|
718
722
|
);
|
|
723
|
+
this.setUserPromise = setUserPromise;
|
|
719
724
|
|
|
720
725
|
try {
|
|
721
|
-
return await
|
|
726
|
+
return await setUserPromise;
|
|
722
727
|
} catch (err) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
+
// disconnectUser() rejects an in-flight handshake so this catch can run *after* something else has taken over.
|
|
729
|
+
// Cleaning up then would tear down whatever replaced us. setUserPromise catches a
|
|
730
|
+
// newer connectUser(); userID additionally catches connectAnonymousUser(), which
|
|
731
|
+
// never sets setUserPromise, and a bare disconnectUser() that already cleaned up.
|
|
732
|
+
if (this.setUserPromise === setUserPromise && this.userID === user.id) {
|
|
733
|
+
if (this.persistUserOnConnectionFailure) {
|
|
734
|
+
// cleanup client to allow the user to retry connectUser again
|
|
735
|
+
this.closeConnection();
|
|
736
|
+
} else {
|
|
737
|
+
this.disconnectUser();
|
|
738
|
+
}
|
|
728
739
|
}
|
|
729
740
|
throw err;
|
|
730
741
|
}
|
|
@@ -852,9 +863,58 @@ export class StreamChat {
|
|
|
852
863
|
}
|
|
853
864
|
|
|
854
865
|
this.clientID = `${this.userID}--${randomId()}`;
|
|
855
|
-
|
|
866
|
+
const wsPromise = this._bindWsPromise(this.connect());
|
|
856
867
|
this._startCleaning();
|
|
857
|
-
return
|
|
868
|
+
return wsPromise;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Rejects the shared `wsPromise`, invalidates any in-flight connect attempt
|
|
873
|
+
*/
|
|
874
|
+
private _rejectPendingWsPromise = (reason: Error) => {
|
|
875
|
+
if (!this._wsPromiseSettled) {
|
|
876
|
+
// invalidate in-flight attempts so a late settle cannot revive this promise
|
|
877
|
+
this._wsConnectId += 1;
|
|
878
|
+
this._wsPromiseSettled = true;
|
|
879
|
+
// Avoid unhandled promise rejection errors
|
|
880
|
+
this.wsPromise?.catch(() => {
|
|
881
|
+
// noop - real awaiters still observe the rejection
|
|
882
|
+
});
|
|
883
|
+
this._rejectWsPromise?.(reason);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Keep a single pending `wsPromise` across close/reopen so callers that already
|
|
889
|
+
* `await this.wsPromise` are not stranded when `openConnection` starts a new connect.
|
|
890
|
+
*/
|
|
891
|
+
private _bindWsPromise = (connectPromise: ConnectAPIResponse): ConnectAPIResponse => {
|
|
892
|
+
const connectId = ++this._wsConnectId;
|
|
893
|
+
|
|
894
|
+
let pending = this.wsPromise;
|
|
895
|
+
if (this._wsPromiseSettled || !pending) {
|
|
896
|
+
this._wsPromiseSettled = false;
|
|
897
|
+
pending = new Promise((resolve, reject) => {
|
|
898
|
+
this._resolveWsPromise = resolve;
|
|
899
|
+
this._rejectWsPromise = reject;
|
|
900
|
+
});
|
|
901
|
+
this.wsPromise = pending;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
connectPromise.then(
|
|
905
|
+
(value) => {
|
|
906
|
+
if (connectId !== this._wsConnectId) return;
|
|
907
|
+
this._wsPromiseSettled = true;
|
|
908
|
+
this._resolveWsPromise?.(value);
|
|
909
|
+
},
|
|
910
|
+
(error) => {
|
|
911
|
+
if (connectId !== this._wsConnectId) return;
|
|
912
|
+
this._wsPromiseSettled = true;
|
|
913
|
+
this._rejectWsPromise?.(error);
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
return pending;
|
|
858
918
|
};
|
|
859
919
|
|
|
860
920
|
/**
|
|
@@ -1059,6 +1119,11 @@ export class StreamChat {
|
|
|
1059
1119
|
|
|
1060
1120
|
this.anonymous = false;
|
|
1061
1121
|
|
|
1122
|
+
this._rejectPendingWsPromise(
|
|
1123
|
+
new Error('Connection was closed because disconnectUser() was called'),
|
|
1124
|
+
);
|
|
1125
|
+
this.wsPromise = null;
|
|
1126
|
+
|
|
1062
1127
|
const closePromise = this.closeConnection(timeout);
|
|
1063
1128
|
|
|
1064
1129
|
for (const channel of Object.values(this.activeChannels)) {
|
|
@@ -1751,7 +1816,12 @@ export class StreamChat {
|
|
|
1751
1816
|
} as Event);
|
|
1752
1817
|
}
|
|
1753
1818
|
|
|
1754
|
-
|
|
1819
|
+
// If state recovery happens afer a failed connect (for example on persistUserOnConnectionFailure: true) we need to flip the wsPromise from rejected to resolved
|
|
1820
|
+
// Otherwise all API calls that wait for the promise will fail
|
|
1821
|
+
// If promise is not yet settled - we leave it for the connect sequence to resolve the promise
|
|
1822
|
+
if (this._wsPromiseSettled) {
|
|
1823
|
+
this.wsPromise = Promise.resolve();
|
|
1824
|
+
}
|
|
1755
1825
|
this.setUserPromise = Promise.resolve();
|
|
1756
1826
|
};
|
|
1757
1827
|
|
|
@@ -5306,6 +5376,10 @@ export class StreamChat {
|
|
|
5306
5376
|
/**
|
|
5307
5377
|
* Update Channels Batch
|
|
5308
5378
|
*
|
|
5379
|
+
* For the `updateData` operation, `data.custom` replaces the channel's whole
|
|
5380
|
+
* custom object, while the root-level `custom_set` / `custom_unset` patch
|
|
5381
|
+
* individual keys and leave the rest untouched.
|
|
5382
|
+
*
|
|
5309
5383
|
* @param {UpdateChannelsBatchOptions} payload for updating channels in batch
|
|
5310
5384
|
* @return {Promise<APIResponse & UpdateChannelsBatchResponse>} The server response
|
|
5311
5385
|
*/
|
|
@@ -32,12 +32,15 @@ export const createLinkPreviewsCompositionMiddleware = (
|
|
|
32
32
|
LinkPreviewsManager.getPreviewData(preview),
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const messageAttachments: Attachment[] = (state.message.attachments ?? []).concat(
|
|
36
36
|
linkPreviews,
|
|
37
37
|
);
|
|
38
|
+
const localAttachments: Attachment[] = (
|
|
39
|
+
state.localMessage.attachments ?? []
|
|
40
|
+
).concat(linkPreviews);
|
|
38
41
|
|
|
39
42
|
// prevent introducing attachments array into the payload sent to the server
|
|
40
|
-
if (!
|
|
43
|
+
if (!messageAttachments.length && !localAttachments.length) return forward();
|
|
41
44
|
|
|
42
45
|
const sendOptions = { ...state.sendOptions };
|
|
43
46
|
const skip_enrich_url =
|
|
@@ -47,16 +50,23 @@ export const createLinkPreviewsCompositionMiddleware = (
|
|
|
47
50
|
sendOptions.skip_enrich_url = true;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
// Each payload gets the `attachments` key only when there is something to put in it,
|
|
54
|
+
// matching the attachments middleware. An empty array is not "nothing to say": on an edit
|
|
55
|
+
// the API reads it as "remove every attachment".
|
|
50
56
|
return next({
|
|
51
57
|
...state,
|
|
52
|
-
message:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
message: messageAttachments.length
|
|
59
|
+
? {
|
|
60
|
+
...state.message,
|
|
61
|
+
attachments: messageAttachments,
|
|
62
|
+
}
|
|
63
|
+
: state.message,
|
|
64
|
+
localMessage: localAttachments.length
|
|
65
|
+
? {
|
|
66
|
+
...state.localMessage,
|
|
67
|
+
attachments: localAttachments,
|
|
68
|
+
}
|
|
69
|
+
: state.localMessage,
|
|
60
70
|
sendOptions,
|
|
61
71
|
});
|
|
62
72
|
},
|