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.
Files changed (39) hide show
  1. package/dist/cjs/index.browser.cjs +397 -21
  2. package/dist/cjs/index.browser.cjs.map +4 -4
  3. package/dist/cjs/index.node.cjs +410 -28
  4. package/dist/cjs/index.node.cjs.map +4 -4
  5. package/dist/esm/index.js +397 -21
  6. package/dist/esm/index.js.map +4 -4
  7. package/dist/types/LiveLocationManager.d.ts +54 -0
  8. package/dist/types/channel.d.ts +3 -1
  9. package/dist/types/channel_manager.d.ts +5 -1
  10. package/dist/types/client.d.ts +7 -6
  11. package/dist/types/events.d.ts +2 -0
  12. package/dist/types/index.d.ts +1 -0
  13. package/dist/types/messageComposer/LocationComposer.d.ts +34 -0
  14. package/dist/types/messageComposer/attachmentIdentity.d.ts +2 -1
  15. package/dist/types/messageComposer/configuration/configuration.d.ts +2 -1
  16. package/dist/types/messageComposer/configuration/types.d.ts +11 -0
  17. package/dist/types/messageComposer/index.d.ts +1 -0
  18. package/dist/types/messageComposer/messageComposer.d.ts +7 -2
  19. package/dist/types/messageComposer/middleware/messageComposer/index.d.ts +1 -0
  20. package/dist/types/messageComposer/middleware/messageComposer/sharedLocation.d.ts +3 -0
  21. package/dist/types/types.d.ts +44 -5
  22. package/package.json +1 -1
  23. package/src/LiveLocationManager.ts +297 -0
  24. package/src/channel.ts +34 -0
  25. package/src/channel_manager.ts +15 -2
  26. package/src/client.ts +14 -5
  27. package/src/events.ts +2 -0
  28. package/src/index.ts +1 -0
  29. package/src/messageComposer/LocationComposer.ts +94 -0
  30. package/src/messageComposer/attachmentIdentity.ts +8 -1
  31. package/src/messageComposer/configuration/configuration.ts +8 -0
  32. package/src/messageComposer/configuration/types.ts +14 -0
  33. package/src/messageComposer/index.ts +1 -0
  34. package/src/messageComposer/messageComposer.ts +81 -9
  35. package/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts +2 -0
  36. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +1 -5
  37. package/src/messageComposer/middleware/messageComposer/index.ts +1 -0
  38. package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +42 -0
  39. 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
  *
@@ -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.client.queryChannels(
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.client.queryChannels(
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
- }) => new ChannelManager({ client: this, eventHandlerOverrides, options });
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 UserLocation the location data to update
4596
+ * @param location SharedLocationRequest the location data to update
4588
4597
  *
4589
- * @returns {Promise<APIResponse>} The server response
4598
+ * @returns {Promise<SharedLocationResponse>} The server response
4590
4599
  */
4591
- async updateLocation(location: SharedLocationRequest) {
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';