stream-chat 9.11.0 → 9.12.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 +397 -21
- package/dist/cjs/index.browser.cjs.map +4 -4
- package/dist/cjs/index.node.cjs +410 -28
- package/dist/cjs/index.node.cjs.map +4 -4
- package/dist/esm/index.js +397 -21
- 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 +7 -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 +44 -5
- package/package.json +1 -1
- package/src/LiveLocationManager.ts +297 -0
- package/src/channel.ts +34 -0
- package/src/channel_manager.ts +15 -2
- package/src/client.ts +14 -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/types.ts +46 -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,
|
|
@@ -407,7 +420,7 @@ export class ChannelManager extends WithSubscriptions {
|
|
|
407
420
|
this.state.partialNext({
|
|
408
421
|
pagination: { ...pagination, isLoading: false, isLoadingNext: true },
|
|
409
422
|
});
|
|
410
|
-
const nextChannels = await this.
|
|
423
|
+
const nextChannels = await this.queryChannelsRequest(
|
|
411
424
|
filters,
|
|
412
425
|
sort,
|
|
413
426
|
options,
|
package/src/client.ts
CHANGED
|
@@ -191,7 +191,6 @@ import type {
|
|
|
191
191
|
SegmentTargetsResponse,
|
|
192
192
|
SegmentType,
|
|
193
193
|
SendFileAPIResponse,
|
|
194
|
-
SharedLocationRequest,
|
|
195
194
|
SharedLocationResponse,
|
|
196
195
|
SortParam,
|
|
197
196
|
StreamChatOptions,
|
|
@@ -209,6 +208,7 @@ import type {
|
|
|
209
208
|
UpdateChannelTypeResponse,
|
|
210
209
|
UpdateCommandOptions,
|
|
211
210
|
UpdateCommandResponse,
|
|
211
|
+
UpdateLocationPayload,
|
|
212
212
|
UpdateMessageAPIResponse,
|
|
213
213
|
UpdateMessageOptions,
|
|
214
214
|
UpdatePollAPIResponse,
|
|
@@ -233,6 +233,7 @@ import { PollManager } from './poll_manager';
|
|
|
233
233
|
import type {
|
|
234
234
|
ChannelManagerEventHandlerOverrides,
|
|
235
235
|
ChannelManagerOptions,
|
|
236
|
+
QueryChannelsRequestType,
|
|
236
237
|
} from './channel_manager';
|
|
237
238
|
import { ChannelManager } from './channel_manager';
|
|
238
239
|
import { NotificationManager } from './notifications';
|
|
@@ -720,10 +721,18 @@ export class StreamChat {
|
|
|
720
721
|
createChannelManager = ({
|
|
721
722
|
eventHandlerOverrides = {},
|
|
722
723
|
options = {},
|
|
724
|
+
queryChannelsOverride,
|
|
723
725
|
}: {
|
|
724
726
|
eventHandlerOverrides?: ChannelManagerEventHandlerOverrides;
|
|
725
727
|
options?: ChannelManagerOptions;
|
|
726
|
-
|
|
728
|
+
queryChannelsOverride?: QueryChannelsRequestType;
|
|
729
|
+
}) =>
|
|
730
|
+
new ChannelManager({
|
|
731
|
+
client: this,
|
|
732
|
+
eventHandlerOverrides,
|
|
733
|
+
options,
|
|
734
|
+
queryChannelsOverride,
|
|
735
|
+
});
|
|
727
736
|
|
|
728
737
|
/**
|
|
729
738
|
* Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
|
|
@@ -4584,11 +4593,11 @@ export class StreamChat {
|
|
|
4584
4593
|
/**
|
|
4585
4594
|
* updateLocation - Updates a location
|
|
4586
4595
|
*
|
|
4587
|
-
* @param location
|
|
4596
|
+
* @param location SharedLocationRequest the location data to update
|
|
4588
4597
|
*
|
|
4589
|
-
* @returns {Promise<
|
|
4598
|
+
* @returns {Promise<SharedLocationResponse>} The server response
|
|
4590
4599
|
*/
|
|
4591
|
-
async updateLocation(location:
|
|
4600
|
+
async updateLocation(location: UpdateLocationPayload) {
|
|
4592
4601
|
return await this.put<SharedLocationResponse>(
|
|
4593
4602
|
this.baseURL + `/users/live_locations`,
|
|
4594
4603
|
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
|
};
|
|
@@ -11,6 +11,7 @@ export type UploadRequestFn = (
|
|
|
11
11
|
export type DraftsConfiguration = {
|
|
12
12
|
enabled: boolean;
|
|
13
13
|
};
|
|
14
|
+
|
|
14
15
|
export type TextComposerConfig = {
|
|
15
16
|
/** If false, the text input, change and selection events are disabled */
|
|
16
17
|
enabled: boolean;
|
|
@@ -23,6 +24,7 @@ export type TextComposerConfig = {
|
|
|
23
24
|
/** Prevents sending a message longer than this length */
|
|
24
25
|
maxLengthOnSend?: number;
|
|
25
26
|
};
|
|
27
|
+
|
|
26
28
|
export type AttachmentManagerConfig = {
|
|
27
29
|
// todo: document removal of noFiles prop showing how to achieve the same with custom fileUploadFilter function
|
|
28
30
|
/**
|
|
@@ -53,6 +55,16 @@ export type LinkPreviewsManagerConfig = {
|
|
|
53
55
|
onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void;
|
|
54
56
|
};
|
|
55
57
|
|
|
58
|
+
export type LocationComposerConfig = {
|
|
59
|
+
/**
|
|
60
|
+
* Allows for toggling the location addition.
|
|
61
|
+
* By default, the feature is enabled but has to be enabled also on channel level config via shared_locations.
|
|
62
|
+
*/
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
/** Function that provides a stable id for a device from which the location is shared */
|
|
65
|
+
getDeviceId: () => string;
|
|
66
|
+
};
|
|
67
|
+
|
|
56
68
|
export type MessageComposerConfig = {
|
|
57
69
|
/** If true, enables creating drafts on the server */
|
|
58
70
|
drafts: DraftsConfiguration;
|
|
@@ -60,6 +72,8 @@ export type MessageComposerConfig = {
|
|
|
60
72
|
attachments: AttachmentManagerConfig;
|
|
61
73
|
/** Configuration for the link previews manager */
|
|
62
74
|
linkPreviews: LinkPreviewsManagerConfig;
|
|
75
|
+
/** Configuration for the location composer */
|
|
76
|
+
location: LocationComposerConfig;
|
|
63
77
|
/** Maximum number of characters in a message */
|
|
64
78
|
text: TextComposerConfig;
|
|
65
79
|
};
|
|
@@ -4,6 +4,7 @@ export * from './configuration';
|
|
|
4
4
|
export * from './CustomDataManager';
|
|
5
5
|
export * from './fileUtils';
|
|
6
6
|
export * from './linkPreviewsManager';
|
|
7
|
+
export * from './LocationComposer';
|
|
7
8
|
export * from './messageComposer';
|
|
8
9
|
export * from './middleware';
|
|
9
10
|
export * from './pollComposer';
|