stream-chat 9.11.0 → 9.13.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.cjs +437 -23
- package/dist/cjs/index.browser.cjs.map +4 -4
- package/dist/cjs/index.node.cjs +451 -30
- package/dist/cjs/index.node.cjs.map +4 -4
- package/dist/esm/index.js +437 -23
- package/dist/esm/index.js.map +4 -4
- package/dist/types/LiveLocationManager.d.ts +54 -0
- package/dist/types/channel.d.ts +3 -1
- package/dist/types/channel_manager.d.ts +5 -1
- package/dist/types/client.d.ts +14 -6
- package/dist/types/events.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messageComposer/LocationComposer.d.ts +34 -0
- package/dist/types/messageComposer/attachmentIdentity.d.ts +2 -1
- package/dist/types/messageComposer/configuration/configuration.d.ts +2 -1
- package/dist/types/messageComposer/configuration/types.d.ts +11 -0
- package/dist/types/messageComposer/index.d.ts +1 -0
- package/dist/types/messageComposer/messageComposer.d.ts +7 -2
- package/dist/types/messageComposer/middleware/messageComposer/index.d.ts +1 -0
- package/dist/types/messageComposer/middleware/messageComposer/sharedLocation.d.ts +3 -0
- package/dist/types/types.d.ts +58 -5
- package/package.json +1 -1
- package/src/LiveLocationManager.ts +297 -0
- package/src/channel.ts +34 -0
- package/src/channel_manager.ts +33 -4
- package/src/client.ts +27 -5
- package/src/events.ts +2 -0
- package/src/index.ts +1 -0
- package/src/messageComposer/LocationComposer.ts +94 -0
- package/src/messageComposer/attachmentIdentity.ts +8 -1
- package/src/messageComposer/configuration/configuration.ts +8 -0
- package/src/messageComposer/configuration/types.ts +14 -0
- package/src/messageComposer/index.ts +1 -0
- package/src/messageComposer/messageComposer.ts +81 -9
- package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +2 -0
- package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +1 -5
- package/src/messageComposer/middleware/messageComposer/index.ts +1 -0
- package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +42 -0
- package/src/offline-support/offline_sync_manager.ts +16 -0
- package/src/types.ts +63 -5
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RULES:
|
|
3
|
+
*
|
|
4
|
+
* 1. one loc-sharing message per channel per user
|
|
5
|
+
* 2. live location is intended to be per device
|
|
6
|
+
* but created_by_device_id has currently no checks,
|
|
7
|
+
* and user can update the location from another device
|
|
8
|
+
* thus making location sharing based on user and channel
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { withCancellation } from './utils/concurrency';
|
|
12
|
+
import { StateStore } from './store';
|
|
13
|
+
import { WithSubscriptions } from './utils/WithSubscriptions';
|
|
14
|
+
import type { StreamChat } from './client';
|
|
15
|
+
import type { Unsubscribe } from './store';
|
|
16
|
+
import type {
|
|
17
|
+
EventTypes,
|
|
18
|
+
MessageResponse,
|
|
19
|
+
SharedLiveLocationResponse,
|
|
20
|
+
SharedLocationResponse,
|
|
21
|
+
} from './types';
|
|
22
|
+
import type { Coords } from './messageComposer';
|
|
23
|
+
|
|
24
|
+
export type WatchLocationHandler = (value: Coords) => void;
|
|
25
|
+
export type WatchLocation = (handler: WatchLocationHandler) => Unsubscribe;
|
|
26
|
+
type DeviceIdGenerator = () => string;
|
|
27
|
+
type MessageId = string;
|
|
28
|
+
|
|
29
|
+
export type ScheduledLiveLocationSharing = SharedLiveLocationResponse & {
|
|
30
|
+
stopSharingTimeout: ReturnType<typeof setTimeout> | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type LiveLocationManagerState = {
|
|
34
|
+
ready: boolean;
|
|
35
|
+
messages: Map<MessageId, ScheduledLiveLocationSharing>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isExpiredLocation = (location: SharedLiveLocationResponse) => {
|
|
39
|
+
const endTimeTimestamp = new Date(location.end_at).getTime();
|
|
40
|
+
|
|
41
|
+
return endTimeTimestamp < Date.now();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function isValidLiveLocationMessage(
|
|
45
|
+
message?: MessageResponse,
|
|
46
|
+
): message is MessageResponse & { shared_location: SharedLiveLocationResponse } {
|
|
47
|
+
if (!message || message.type === 'deleted' || !message.shared_location?.end_at)
|
|
48
|
+
return false;
|
|
49
|
+
|
|
50
|
+
return !isExpiredLocation(message.shared_location as SharedLiveLocationResponse);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type LiveLocationManagerConstructorParameters = {
|
|
54
|
+
client: StreamChat;
|
|
55
|
+
getDeviceId: DeviceIdGenerator;
|
|
56
|
+
watchLocation: WatchLocation;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Hard-coded minimal throttle timeout
|
|
60
|
+
export const UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT = 3000;
|
|
61
|
+
|
|
62
|
+
export class LiveLocationManager extends WithSubscriptions {
|
|
63
|
+
public state: StateStore<LiveLocationManagerState>;
|
|
64
|
+
private client: StreamChat;
|
|
65
|
+
private getDeviceId: DeviceIdGenerator;
|
|
66
|
+
private _deviceId: string;
|
|
67
|
+
private watchLocation: WatchLocation;
|
|
68
|
+
|
|
69
|
+
static symbol = Symbol(LiveLocationManager.name);
|
|
70
|
+
|
|
71
|
+
constructor({
|
|
72
|
+
client,
|
|
73
|
+
getDeviceId,
|
|
74
|
+
watchLocation,
|
|
75
|
+
}: LiveLocationManagerConstructorParameters) {
|
|
76
|
+
if (!client.userID) {
|
|
77
|
+
throw new Error('Live-location sharing is reserved for client-side use only');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
super();
|
|
81
|
+
|
|
82
|
+
this.client = client;
|
|
83
|
+
this.state = new StateStore<LiveLocationManagerState>({
|
|
84
|
+
messages: new Map(),
|
|
85
|
+
ready: false,
|
|
86
|
+
});
|
|
87
|
+
this._deviceId = getDeviceId();
|
|
88
|
+
this.getDeviceId = getDeviceId;
|
|
89
|
+
this.watchLocation = watchLocation;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public async init() {
|
|
93
|
+
await this.assureStateInit();
|
|
94
|
+
this.registerSubscriptions();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public registerSubscriptions = () => {
|
|
98
|
+
this.incrementRefCount();
|
|
99
|
+
if (this.hasSubscriptions) return;
|
|
100
|
+
|
|
101
|
+
this.addUnsubscribeFunction(this.subscribeLiveLocationSharingUpdates());
|
|
102
|
+
this.addUnsubscribeFunction(this.subscribeTargetMessagesChange());
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
public unregisterSubscriptions = () => super.unregisterSubscriptions();
|
|
106
|
+
|
|
107
|
+
get messages() {
|
|
108
|
+
return this.state.getLatestValue().messages;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get stateIsReady() {
|
|
112
|
+
return this.state.getLatestValue().ready;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get deviceId() {
|
|
116
|
+
if (!this._deviceId) {
|
|
117
|
+
this._deviceId = this.getDeviceId();
|
|
118
|
+
}
|
|
119
|
+
return this._deviceId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async assureStateInit() {
|
|
123
|
+
if (this.stateIsReady) return;
|
|
124
|
+
const { active_live_locations } = await this.client.getSharedLocations();
|
|
125
|
+
this.state.next({
|
|
126
|
+
messages: new Map(
|
|
127
|
+
active_live_locations
|
|
128
|
+
.filter((location) => !isExpiredLocation(location))
|
|
129
|
+
.map((location) => [
|
|
130
|
+
location.message_id,
|
|
131
|
+
{
|
|
132
|
+
...location,
|
|
133
|
+
stopSharingTimeout: setTimeout(
|
|
134
|
+
() => {
|
|
135
|
+
this.unregisterMessages([location.message_id]);
|
|
136
|
+
},
|
|
137
|
+
new Date(location.end_at).getTime() - Date.now(),
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
]),
|
|
141
|
+
),
|
|
142
|
+
ready: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private subscribeTargetMessagesChange() {
|
|
147
|
+
let unsubscribeWatchLocation: null | (() => void) = null;
|
|
148
|
+
|
|
149
|
+
// Subscribe to location updates only if there are relevant messages to
|
|
150
|
+
// update, no need for the location watcher to be active/instantiated otherwise
|
|
151
|
+
const unsubscribe = this.state.subscribeWithSelector(
|
|
152
|
+
({ messages }) => ({ messages }),
|
|
153
|
+
({ messages }) => {
|
|
154
|
+
if (!messages.size) {
|
|
155
|
+
unsubscribeWatchLocation?.();
|
|
156
|
+
unsubscribeWatchLocation = null;
|
|
157
|
+
} else if (messages.size && !unsubscribeWatchLocation) {
|
|
158
|
+
unsubscribeWatchLocation = this.subscribeWatchLocation();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
unsubscribe();
|
|
165
|
+
unsubscribeWatchLocation?.();
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private subscribeWatchLocation() {
|
|
170
|
+
let nextAllowedUpdateCallTimestamp = Date.now();
|
|
171
|
+
|
|
172
|
+
const unsubscribe = this.watchLocation(({ latitude, longitude }) => {
|
|
173
|
+
// Integrators can adjust the update interval by supplying custom watchLocation subscription,
|
|
174
|
+
// but the minimal timeout still has to be set as a failsafe (to prevent rate-limitting)
|
|
175
|
+
if (Date.now() < nextAllowedUpdateCallTimestamp) return;
|
|
176
|
+
|
|
177
|
+
nextAllowedUpdateCallTimestamp =
|
|
178
|
+
Date.now() + UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT;
|
|
179
|
+
|
|
180
|
+
withCancellation(LiveLocationManager.symbol, async () => {
|
|
181
|
+
const promises: Promise<SharedLocationResponse>[] = [];
|
|
182
|
+
await this.assureStateInit();
|
|
183
|
+
const expiredLocations: string[] = [];
|
|
184
|
+
|
|
185
|
+
for (const [messageId, location] of this.messages) {
|
|
186
|
+
if (isExpiredLocation(location)) {
|
|
187
|
+
expiredLocations.push(location.message_id);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (location.latitude === latitude && location.longitude === longitude)
|
|
191
|
+
continue;
|
|
192
|
+
const promise = this.client.updateLocation({
|
|
193
|
+
created_by_device_id: location.created_by_device_id,
|
|
194
|
+
message_id: messageId,
|
|
195
|
+
latitude,
|
|
196
|
+
longitude,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
promises.push(promise);
|
|
200
|
+
}
|
|
201
|
+
this.unregisterMessages(expiredLocations);
|
|
202
|
+
if (promises.length > 0) {
|
|
203
|
+
await Promise.allSettled(promises);
|
|
204
|
+
}
|
|
205
|
+
// TODO: handle values (remove failed - based on specific error code), keep re-trying others
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return unsubscribe;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private subscribeLiveLocationSharingUpdates() {
|
|
213
|
+
/**
|
|
214
|
+
* Both message.updated & live_location_sharing.stopped get emitted when message gets an
|
|
215
|
+
* update, live_location_sharing.stopped gets emitted only locally and only if the update goes
|
|
216
|
+
* through, it's a failsafe for when channel is no longer being watched for whatever reason
|
|
217
|
+
*/
|
|
218
|
+
const subscriptions = [
|
|
219
|
+
...(
|
|
220
|
+
[
|
|
221
|
+
'live_location_sharing.started',
|
|
222
|
+
'message.updated',
|
|
223
|
+
'message.deleted',
|
|
224
|
+
] as EventTypes[]
|
|
225
|
+
).map((eventType) =>
|
|
226
|
+
this.client.on(eventType, (event) => {
|
|
227
|
+
if (!event.message) return;
|
|
228
|
+
|
|
229
|
+
if (event.type === 'live_location_sharing.started') {
|
|
230
|
+
this.registerMessage(event.message);
|
|
231
|
+
} else if (event.type === 'message.updated') {
|
|
232
|
+
const isRegistered = this.messages.has(event.message.id);
|
|
233
|
+
if (isRegistered && !isValidLiveLocationMessage(event.message)) {
|
|
234
|
+
this.unregisterMessages([event.message.id]);
|
|
235
|
+
}
|
|
236
|
+
this.registerMessage(event.message);
|
|
237
|
+
} else {
|
|
238
|
+
this.unregisterMessages([event.message.id]);
|
|
239
|
+
}
|
|
240
|
+
}),
|
|
241
|
+
),
|
|
242
|
+
this.client.on('live_location_sharing.stopped', (event) => {
|
|
243
|
+
if (!event.live_location) return;
|
|
244
|
+
|
|
245
|
+
this.unregisterMessages([event.live_location?.message_id]);
|
|
246
|
+
}),
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private registerMessage(message: MessageResponse) {
|
|
253
|
+
if (
|
|
254
|
+
!this.client.userID ||
|
|
255
|
+
message?.user?.id !== this.client.userID ||
|
|
256
|
+
!isValidLiveLocationMessage(message)
|
|
257
|
+
)
|
|
258
|
+
return;
|
|
259
|
+
|
|
260
|
+
this.state.next((currentValue) => {
|
|
261
|
+
const messages = new Map(currentValue.messages);
|
|
262
|
+
messages.set(message.id, {
|
|
263
|
+
...message.shared_location,
|
|
264
|
+
stopSharingTimeout: setTimeout(
|
|
265
|
+
() => {
|
|
266
|
+
this.unregisterMessages([message.id]);
|
|
267
|
+
},
|
|
268
|
+
new Date(message.shared_location.end_at).getTime() - Date.now(),
|
|
269
|
+
),
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
...currentValue,
|
|
273
|
+
messages,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private unregisterMessages(messageIds: string[]) {
|
|
279
|
+
const messages = this.messages;
|
|
280
|
+
const removedMessages = new Set(messageIds);
|
|
281
|
+
const newMessages = new Map(
|
|
282
|
+
Array.from(messages).filter(([messageId, location]) => {
|
|
283
|
+
if (removedMessages.has(messageId) && location.stopSharingTimeout) {
|
|
284
|
+
clearTimeout(location.stopSharingTimeout);
|
|
285
|
+
location.stopSharingTimeout = null;
|
|
286
|
+
}
|
|
287
|
+
return !removedMessages.has(messageId);
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (newMessages.size === messages.size) return;
|
|
292
|
+
|
|
293
|
+
this.state.partialNext({
|
|
294
|
+
messages: newMessages,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
package/src/channel.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
GetMultipleMessagesAPIResponse,
|
|
32
32
|
GetReactionsAPIResponse,
|
|
33
33
|
GetRepliesAPIResponse,
|
|
34
|
+
LiveLocationPayload,
|
|
34
35
|
LocalMessage,
|
|
35
36
|
MarkReadOptions,
|
|
36
37
|
MarkUnreadOptions,
|
|
@@ -63,10 +64,12 @@ import type {
|
|
|
63
64
|
SendMessageAPIResponse,
|
|
64
65
|
SendMessageOptions,
|
|
65
66
|
SendReactionOptions,
|
|
67
|
+
StaticLocationPayload,
|
|
66
68
|
TruncateChannelAPIResponse,
|
|
67
69
|
TruncateOptions,
|
|
68
70
|
UpdateChannelAPIResponse,
|
|
69
71
|
UpdateChannelOptions,
|
|
72
|
+
UpdateLocationPayload,
|
|
70
73
|
UserResponse,
|
|
71
74
|
} from './types';
|
|
72
75
|
import type { Role } from './permissions';
|
|
@@ -669,6 +672,37 @@ export class Channel {
|
|
|
669
672
|
return data;
|
|
670
673
|
}
|
|
671
674
|
|
|
675
|
+
public async sendSharedLocation(
|
|
676
|
+
location: StaticLocationPayload | LiveLocationPayload,
|
|
677
|
+
userId?: string,
|
|
678
|
+
) {
|
|
679
|
+
const result = await this.sendMessage({
|
|
680
|
+
id: location.message_id,
|
|
681
|
+
shared_location: location,
|
|
682
|
+
user: userId ? { id: userId } : undefined,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
if ((location as LiveLocationPayload).end_at) {
|
|
686
|
+
this.getClient().dispatchEvent({
|
|
687
|
+
message: result.message,
|
|
688
|
+
type: 'live_location_sharing.started',
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
public async stopLiveLocationSharing(payload: UpdateLocationPayload) {
|
|
696
|
+
const location = await this.getClient().updateLocation({
|
|
697
|
+
...payload,
|
|
698
|
+
end_at: new Date().toISOString(),
|
|
699
|
+
});
|
|
700
|
+
this.getClient().dispatchEvent({
|
|
701
|
+
live_location: location,
|
|
702
|
+
type: 'live_location_sharing.stopped',
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
672
706
|
/**
|
|
673
707
|
* delete - Delete the channel. Messages are permanently removed.
|
|
674
708
|
*
|
package/src/channel_manager.ts
CHANGED
|
@@ -133,6 +133,10 @@ export type ChannelManagerOptions = {
|
|
|
133
133
|
lockChannelOrder?: boolean;
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
export type QueryChannelsRequestType = (
|
|
137
|
+
...params: Parameters<StreamChat['queryChannels']>
|
|
138
|
+
) => Promise<Channel[]>;
|
|
139
|
+
|
|
136
140
|
export const DEFAULT_CHANNEL_MANAGER_OPTIONS = {
|
|
137
141
|
abortInFlightQuery: false,
|
|
138
142
|
allowNotLoadedChannelPromotionForEvent: {
|
|
@@ -160,6 +164,7 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
160
164
|
private client: StreamChat;
|
|
161
165
|
private eventHandlers: Map<string, EventHandlerType> = new Map();
|
|
162
166
|
private eventHandlerOverrides: Map<string, EventHandlerOverrideType> = new Map();
|
|
167
|
+
private queryChannelsRequest: QueryChannelsRequestType;
|
|
163
168
|
private options: ChannelManagerOptions = {};
|
|
164
169
|
private stateOptions: ChannelStateOptions = {};
|
|
165
170
|
private id: string;
|
|
@@ -168,10 +173,12 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
168
173
|
client,
|
|
169
174
|
eventHandlerOverrides = {},
|
|
170
175
|
options = {},
|
|
176
|
+
queryChannelsOverride,
|
|
171
177
|
}: {
|
|
172
178
|
client: StreamChat;
|
|
173
179
|
eventHandlerOverrides?: ChannelManagerEventHandlerOverrides;
|
|
174
180
|
options?: ChannelManagerOptions;
|
|
181
|
+
queryChannelsOverride?: QueryChannelsRequestType;
|
|
175
182
|
}) {
|
|
176
183
|
super();
|
|
177
184
|
|
|
@@ -192,6 +199,8 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
192
199
|
});
|
|
193
200
|
this.setEventHandlerOverrides(eventHandlerOverrides);
|
|
194
201
|
this.setOptions(options);
|
|
202
|
+
this.queryChannelsRequest =
|
|
203
|
+
queryChannelsOverride ?? ((...params) => this.client.queryChannels(...params));
|
|
195
204
|
this.eventHandlers = new Map(
|
|
196
205
|
Object.entries<EventHandlerType>({
|
|
197
206
|
channelDeletedHandler: this.channelDeletedHandler,
|
|
@@ -252,6 +261,10 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
252
261
|
);
|
|
253
262
|
};
|
|
254
263
|
|
|
264
|
+
public setQueryChannelsRequest = (queryChannelsRequest: QueryChannelsRequestType) => {
|
|
265
|
+
this.queryChannelsRequest = queryChannelsRequest;
|
|
266
|
+
};
|
|
267
|
+
|
|
255
268
|
public setOptions = (options: ChannelManagerOptions = {}) => {
|
|
256
269
|
this.options = { ...DEFAULT_CHANNEL_MANAGER_OPTIONS, ...options };
|
|
257
270
|
};
|
|
@@ -266,7 +279,7 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
266
279
|
...options,
|
|
267
280
|
};
|
|
268
281
|
try {
|
|
269
|
-
const channels = await this.
|
|
282
|
+
const channels = await this.queryChannelsRequest(
|
|
270
283
|
filters,
|
|
271
284
|
sort,
|
|
272
285
|
options,
|
|
@@ -304,7 +317,19 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
304
317
|
`Maximum number of retries reached in queryChannels. Last error message is: ${err}`,
|
|
305
318
|
);
|
|
306
319
|
|
|
307
|
-
this.state.
|
|
320
|
+
const state = this.state.getLatestValue();
|
|
321
|
+
// If the offline support is enabled, and there are channels in the DB, we should not error out.
|
|
322
|
+
const isOfflineSupportEnabledWithChannels =
|
|
323
|
+
this.client.offlineDb && state.channels.length > 0;
|
|
324
|
+
|
|
325
|
+
this.state.partialNext({
|
|
326
|
+
error: isOfflineSupportEnabledWithChannels ? undefined : wrappedError,
|
|
327
|
+
pagination: {
|
|
328
|
+
...state.pagination,
|
|
329
|
+
isLoading: false,
|
|
330
|
+
isLoadingNext: false,
|
|
331
|
+
},
|
|
332
|
+
});
|
|
308
333
|
return;
|
|
309
334
|
}
|
|
310
335
|
|
|
@@ -407,7 +432,7 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
407
432
|
this.state.partialNext({
|
|
408
433
|
pagination: { ...pagination, isLoading: false, isLoadingNext: true },
|
|
409
434
|
});
|
|
410
|
-
const nextChannels = await this.
|
|
435
|
+
const nextChannels = await this.queryChannelsRequest(
|
|
411
436
|
filters,
|
|
412
437
|
sort,
|
|
413
438
|
options,
|
|
@@ -431,7 +456,11 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
431
456
|
this.client.logger('error', (error as Error).message);
|
|
432
457
|
this.state.next((currentState) => ({
|
|
433
458
|
...currentState,
|
|
434
|
-
pagination: {
|
|
459
|
+
pagination: {
|
|
460
|
+
...currentState.pagination,
|
|
461
|
+
isLoadingNext: false,
|
|
462
|
+
isLoading: false,
|
|
463
|
+
},
|
|
435
464
|
}));
|
|
436
465
|
throw error;
|
|
437
466
|
}
|
package/src/client.ts
CHANGED
|
@@ -106,6 +106,7 @@ import type {
|
|
|
106
106
|
GetCampaignOptions,
|
|
107
107
|
GetChannelTypeResponse,
|
|
108
108
|
GetCommandResponse,
|
|
109
|
+
GetHookEventsResponse,
|
|
109
110
|
GetImportResponse,
|
|
110
111
|
GetMessageAPIResponse,
|
|
111
112
|
GetMessageOptions,
|
|
@@ -148,6 +149,7 @@ import type {
|
|
|
148
149
|
PollVote,
|
|
149
150
|
PollVoteData,
|
|
150
151
|
PollVotesAPIResponse,
|
|
152
|
+
Product,
|
|
151
153
|
PushPreference,
|
|
152
154
|
PushProvider,
|
|
153
155
|
PushProviderConfig,
|
|
@@ -191,7 +193,6 @@ import type {
|
|
|
191
193
|
SegmentTargetsResponse,
|
|
192
194
|
SegmentType,
|
|
193
195
|
SendFileAPIResponse,
|
|
194
|
-
SharedLocationRequest,
|
|
195
196
|
SharedLocationResponse,
|
|
196
197
|
SortParam,
|
|
197
198
|
StreamChatOptions,
|
|
@@ -209,6 +210,7 @@ import type {
|
|
|
209
210
|
UpdateChannelTypeResponse,
|
|
210
211
|
UpdateCommandOptions,
|
|
211
212
|
UpdateCommandResponse,
|
|
213
|
+
UpdateLocationPayload,
|
|
212
214
|
UpdateMessageAPIResponse,
|
|
213
215
|
UpdateMessageOptions,
|
|
214
216
|
UpdatePollAPIResponse,
|
|
@@ -233,6 +235,7 @@ import { PollManager } from './poll_manager';
|
|
|
233
235
|
import type {
|
|
234
236
|
ChannelManagerEventHandlerOverrides,
|
|
235
237
|
ChannelManagerOptions,
|
|
238
|
+
QueryChannelsRequestType,
|
|
236
239
|
} from './channel_manager';
|
|
237
240
|
import { ChannelManager } from './channel_manager';
|
|
238
241
|
import { NotificationManager } from './notifications';
|
|
@@ -720,10 +723,18 @@ export class StreamChat {
|
|
|
720
723
|
createChannelManager = ({
|
|
721
724
|
eventHandlerOverrides = {},
|
|
722
725
|
options = {},
|
|
726
|
+
queryChannelsOverride,
|
|
723
727
|
}: {
|
|
724
728
|
eventHandlerOverrides?: ChannelManagerEventHandlerOverrides;
|
|
725
729
|
options?: ChannelManagerOptions;
|
|
726
|
-
|
|
730
|
+
queryChannelsOverride?: QueryChannelsRequestType;
|
|
731
|
+
}) =>
|
|
732
|
+
new ChannelManager({
|
|
733
|
+
client: this,
|
|
734
|
+
eventHandlerOverrides,
|
|
735
|
+
options,
|
|
736
|
+
queryChannelsOverride,
|
|
737
|
+
});
|
|
727
738
|
|
|
728
739
|
/**
|
|
729
740
|
* Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
|
|
@@ -2155,6 +2166,17 @@ export class StreamChat {
|
|
|
2155
2166
|
});
|
|
2156
2167
|
}
|
|
2157
2168
|
|
|
2169
|
+
/**
|
|
2170
|
+
* getHookEvents - Get available events for hooks (webhook, SQS, and SNS)
|
|
2171
|
+
*
|
|
2172
|
+
* @param {Product[]} [products] Optional array of products to filter events by (e.g., [Product.Chat, Product.Video])
|
|
2173
|
+
* @returns {Promise<GetHookEventsResponse>} Response containing available hook events
|
|
2174
|
+
*/
|
|
2175
|
+
async getHookEvents(products?: Product[]) {
|
|
2176
|
+
const params = products && products.length > 0 ? { product: products.join(',') } : {};
|
|
2177
|
+
return await this.get<GetHookEventsResponse>(this.baseURL + '/hook/events', params);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2158
2180
|
_addChannelConfig({ cid, config }: ChannelResponse) {
|
|
2159
2181
|
if (this._cacheEnabled()) {
|
|
2160
2182
|
this.configs[cid] = config;
|
|
@@ -4584,11 +4606,11 @@ export class StreamChat {
|
|
|
4584
4606
|
/**
|
|
4585
4607
|
* updateLocation - Updates a location
|
|
4586
4608
|
*
|
|
4587
|
-
* @param location
|
|
4609
|
+
* @param location SharedLocationRequest the location data to update
|
|
4588
4610
|
*
|
|
4589
|
-
* @returns {Promise<
|
|
4611
|
+
* @returns {Promise<SharedLocationResponse>} The server response
|
|
4590
4612
|
*/
|
|
4591
|
-
async updateLocation(location:
|
|
4613
|
+
async updateLocation(location: UpdateLocationPayload) {
|
|
4592
4614
|
return await this.put<SharedLocationResponse>(
|
|
4593
4615
|
this.baseURL + `/users/live_locations`,
|
|
4594
4616
|
location,
|
package/src/events.ts
CHANGED
|
@@ -63,6 +63,8 @@ export const EVENT_MAP = {
|
|
|
63
63
|
'connection.recovered': true,
|
|
64
64
|
'transport.changed': true,
|
|
65
65
|
'capabilities.changed': true,
|
|
66
|
+
'live_location_sharing.started': true,
|
|
67
|
+
'live_location_sharing.stopped': true,
|
|
66
68
|
|
|
67
69
|
// Reminder events
|
|
68
70
|
'reminder.created': true,
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export * from './token_manager';
|
|
|
32
32
|
export * from './types';
|
|
33
33
|
export * from './channel_manager';
|
|
34
34
|
export * from './offline-support';
|
|
35
|
+
export * from './LiveLocationManager';
|
|
35
36
|
// Don't use * here, that can break module augmentation https://github.com/microsoft/TypeScript/issues/46617
|
|
36
37
|
export type {
|
|
37
38
|
CustomAttachmentData,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { StateStore } from '../store';
|
|
2
|
+
import type { MessageComposer } from './messageComposer';
|
|
3
|
+
import type {
|
|
4
|
+
DraftMessage,
|
|
5
|
+
LiveLocationPayload,
|
|
6
|
+
LocalMessage,
|
|
7
|
+
StaticLocationPayload,
|
|
8
|
+
} from '../types';
|
|
9
|
+
|
|
10
|
+
export type Coords = { latitude: number; longitude: number };
|
|
11
|
+
|
|
12
|
+
export type LocationComposerOptions = {
|
|
13
|
+
composer: MessageComposer;
|
|
14
|
+
message?: DraftMessage | LocalMessage;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type StaticLocationPreview = StaticLocationPayload;
|
|
18
|
+
|
|
19
|
+
export type LiveLocationPreview = Omit<LiveLocationPayload, 'end_at'> & {
|
|
20
|
+
durationMs?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type LocationComposerState = {
|
|
24
|
+
location: StaticLocationPreview | LiveLocationPreview | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const MIN_LIVE_LOCATION_SHARE_DURATION = 60 * 1000; // 1 minute;
|
|
28
|
+
|
|
29
|
+
const initState = ({
|
|
30
|
+
message,
|
|
31
|
+
}: {
|
|
32
|
+
message?: DraftMessage | LocalMessage;
|
|
33
|
+
}): LocationComposerState => ({
|
|
34
|
+
location: message?.shared_location ?? null,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export class LocationComposer {
|
|
38
|
+
readonly state: StateStore<LocationComposerState>;
|
|
39
|
+
readonly composer: MessageComposer;
|
|
40
|
+
private _deviceId: string;
|
|
41
|
+
|
|
42
|
+
constructor({ composer, message }: LocationComposerOptions) {
|
|
43
|
+
this.composer = composer;
|
|
44
|
+
this.state = new StateStore<LocationComposerState>(initState({ message }));
|
|
45
|
+
this._deviceId = this.config.getDeviceId();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get config() {
|
|
49
|
+
return this.composer.config.location;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get deviceId() {
|
|
53
|
+
return this._deviceId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get location() {
|
|
57
|
+
return this.state.getLatestValue().location;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get validLocation(): StaticLocationPayload | LiveLocationPayload | null {
|
|
61
|
+
const { durationMs, ...location } = (this.location ?? {}) as LiveLocationPreview;
|
|
62
|
+
if (
|
|
63
|
+
!!location?.created_by_device_id &&
|
|
64
|
+
location.message_id &&
|
|
65
|
+
location.latitude &&
|
|
66
|
+
location.longitude &&
|
|
67
|
+
(typeof durationMs === 'undefined' ||
|
|
68
|
+
durationMs >= MIN_LIVE_LOCATION_SHARE_DURATION)
|
|
69
|
+
) {
|
|
70
|
+
return {
|
|
71
|
+
...location,
|
|
72
|
+
end_at: durationMs && new Date(Date.now() + durationMs).toISOString(),
|
|
73
|
+
} as StaticLocationPayload | LiveLocationPayload;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => {
|
|
79
|
+
this.state.next(initState({ message }));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
setData = (data: { durationMs?: number } & Coords) => {
|
|
83
|
+
if (!this.config.enabled) return;
|
|
84
|
+
if (!data.latitude || !data.longitude) return;
|
|
85
|
+
|
|
86
|
+
this.state.partialNext({
|
|
87
|
+
location: {
|
|
88
|
+
...data,
|
|
89
|
+
message_id: this.composer.id,
|
|
90
|
+
created_by_device_id: this.deviceId,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Attachment } from '../types';
|
|
1
|
+
import type { Attachment, SharedLocationResponse } from '../types';
|
|
2
2
|
import type {
|
|
3
3
|
AudioAttachment,
|
|
4
4
|
FileAttachment,
|
|
@@ -90,3 +90,10 @@ export const isUploadedAttachment = (
|
|
|
90
90
|
isImageAttachment(attachment) ||
|
|
91
91
|
isVideoAttachment(attachment) ||
|
|
92
92
|
isVoiceRecordingAttachment(attachment);
|
|
93
|
+
|
|
94
|
+
export const isSharedLocationResponse = (
|
|
95
|
+
location: unknown,
|
|
96
|
+
): location is SharedLocationResponse =>
|
|
97
|
+
!!(location as SharedLocationResponse).latitude &&
|
|
98
|
+
!!(location as SharedLocationResponse).longitude &&
|
|
99
|
+
!!(location as SharedLocationResponse).channel_cid;
|
|
@@ -3,9 +3,11 @@ import { API_MAX_FILES_ALLOWED_PER_MESSAGE } from '../../constants';
|
|
|
3
3
|
import type {
|
|
4
4
|
AttachmentManagerConfig,
|
|
5
5
|
LinkPreviewsManagerConfig,
|
|
6
|
+
LocationComposerConfig,
|
|
6
7
|
MessageComposerConfig,
|
|
7
8
|
} from './types';
|
|
8
9
|
import type { TextComposerConfig } from './types';
|
|
10
|
+
import { generateUUIDv4 } from '../../utils';
|
|
9
11
|
|
|
10
12
|
export const DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: LinkPreviewsManagerConfig = {
|
|
11
13
|
debounceURLEnrichmentMs: 1500,
|
|
@@ -36,9 +38,15 @@ export const DEFAULT_TEXT_COMPOSER_CONFIG: TextComposerConfig = {
|
|
|
36
38
|
publishTypingEvents: true,
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
export const DEFAULT_LOCATION_COMPOSER_CONFIG: LocationComposerConfig = {
|
|
42
|
+
enabled: true,
|
|
43
|
+
getDeviceId: () => generateUUIDv4(),
|
|
44
|
+
};
|
|
45
|
+
|
|
39
46
|
export const DEFAULT_COMPOSER_CONFIG: MessageComposerConfig = {
|
|
40
47
|
attachments: DEFAULT_ATTACHMENT_MANAGER_CONFIG,
|
|
41
48
|
drafts: { enabled: false },
|
|
42
49
|
linkPreviews: DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
|
|
50
|
+
location: DEFAULT_LOCATION_COMPOSER_CONFIG,
|
|
43
51
|
text: DEFAULT_TEXT_COMPOSER_CONFIG,
|
|
44
52
|
};
|