polfan-server-js-client 0.2.96 → 0.2.98

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.
@@ -1,42 +1,25 @@
1
1
  import {ChatStateTracker} from "./ChatStateTracker";
2
2
  import {
3
- NewMessage,
4
- FollowedTopic,
5
- TopicFollowed,
6
3
  RoomDeleted,
7
4
  RoomLeft,
8
- TopicDeleted,
9
- FollowedTopicUpdated,
10
5
  RoomJoined,
11
- NewTopic,
12
6
  Session,
13
7
  Room,
14
8
  Message, ChatLocation,
15
9
  } from "../types/src";
16
- import {
17
- IndexedCollection,
18
- ObservableIndexedObjectCollection
19
- } from "../IndexedObjectCollection";
20
- import {DeferredTask, PromiseRegistry} from "./AsyncUtils";
10
+ import {IndexedCollection} from "../IndexedObjectCollection";
11
+ import {DeferredTask} from "./AsyncUtils";
21
12
  import {RoomMessagesHistory} from "./RoomMessagesHistory";
22
13
 
23
14
  export class MessagesManager {
24
15
  private readonly roomHistories = new IndexedCollection<string, RoomMessagesHistory>();
25
- private readonly followedTopics = new IndexedCollection<string, ObservableIndexedObjectCollection<FollowedTopic>>();
26
- private readonly followedTopicsPromises = new PromiseRegistry();
27
16
  private readonly deferredSession = new DeferredTask();
28
- private readonly unreadSummariesCache = new Map<string, { mentionCount: number, isUnread: boolean }>();
29
17
 
30
18
  public constructor(private tracker: ChatStateTracker) {
31
19
  this.tracker.client.on('Session', ev => this.handleSession(ev));
32
20
  this.tracker.client.on('RoomJoined', ev => this.handleRoomJoin(ev));
33
- this.tracker.client.on('NewTopic', ev => this.handleNewTopic(ev));
34
- this.tracker.client.on('FollowedTopicUpdated', ev => this.handleFollowedTopicUpdated(ev));
35
- this.tracker.client.on('TopicFollowed', ev => this.handleTopicFollowed(ev));
36
- this.tracker.client.on('NewMessage', ev => this.handleNewMessage(ev));
37
21
  this.tracker.client.on('RoomDeleted', ev => this.handleRoomDeleted(ev));
38
22
  this.tracker.client.on('RoomLeft', ev => this.handleRoomLeft(ev));
39
- this.tracker.client.on('TopicDeleted', ev => this.handleTopicDeleted(ev));
40
23
  }
41
24
 
42
25
  /**
@@ -47,164 +30,6 @@ export class MessagesManager {
47
30
  return this.roomHistories.get(roomId);
48
31
  }
49
32
 
50
- /**
51
- * Cache followed topics for all joined rooms in a space and fetch them in bulk if necessary.
52
- * Then you can get them using getRoomFollowedTopics().
53
- * @see getRoomFollowedTopics
54
- */
55
- public async cacheSpaceFollowedTopics(spaceId: string | null): Promise<void> {
56
- if (spaceId && ! (await this.tracker.spaces.get()).has(spaceId)) {
57
- throw new Error(`You are not in space ${spaceId}`);
58
- }
59
-
60
- const rooms = await this.tracker.rooms.get();
61
- const roomIds = spaceId
62
- ? rooms.findBy('spaceId', spaceId).items.map(r => r.id)
63
- : rooms.items.filter(r => !r.spaceId).map(r => r.id);
64
-
65
- if (!roomIds.length) {
66
- return;
67
- }
68
-
69
- const isAlreadyCached = roomIds.every(roomId => this.followedTopics.has(roomId));
70
- if (isAlreadyCached) {
71
- return;
72
- }
73
-
74
- const spaceRegistryKey = `space_fetch_${spaceId || 'spaceless'}`;
75
-
76
- if (this.followedTopicsPromises.notExist(spaceRegistryKey)) {
77
- this.followedTopicsPromises.registerByFunction(async () => {
78
- const result = await this.tracker.client.send('GetFollowedTopics', {location: {spaceId}});
79
- if (result.error) throw result.error;
80
-
81
- this.setFollowedTopicsArray(roomIds, result.data.followedTopics);
82
- }, spaceRegistryKey);
83
- }
84
-
85
- await this.followedTopicsPromises.get(spaceRegistryKey);
86
- }
87
-
88
- /**
89
- * Get followed topics for the given room.
90
- * @return Undefined if you are not in the room, collection otherwise.
91
- */
92
- public async getRoomFollowedTopics(roomId: string): Promise<ObservableIndexedObjectCollection<FollowedTopic> | undefined> {
93
- if (! (await this.tracker.rooms.get()).has(roomId)) {
94
- return undefined;
95
- }
96
-
97
- if (! this.followedTopics.has(roomId)) {
98
- if (this.followedTopicsPromises.notExist(roomId)) {
99
- this.followedTopicsPromises.registerByFunction(async () => {
100
- const result = await this.tracker.client.send('GetFollowedTopics', {location: {roomId}});
101
-
102
- if (result.error) {
103
- throw result.error;
104
- }
105
-
106
- this.setFollowedTopicsArray([roomId], result.data.followedTopics);
107
- }, roomId);
108
- }
109
-
110
- await this.followedTopicsPromises.get(roomId);
111
- }
112
-
113
- return this.followedTopics.get(roomId);
114
- }
115
-
116
- /**
117
- * Batch acknowledge all messages for given room.
118
- */
119
- public async ackRoom(roomId: string): Promise<void> {
120
- const collection = await this.getRoomFollowedTopics(roomId);
121
-
122
- if (! collection) {
123
- return;
124
- }
125
-
126
- for (const followedTopic of collection.items) {
127
- if (followedTopic.isUnread) {
128
- await this.tracker.client.send('Ack', {location: followedTopic.location});
129
- }
130
- }
131
- }
132
-
133
- /**
134
- * Calculate missed messages with mentions from any topic in given room.
135
- * @return Undefined if you are not in room.
136
- */
137
- public async summarizeUnreadMessages(location: ChatLocation): Promise<{ mentionCount: number, isUnread: boolean }> {
138
- const cacheKey = location.topicId
139
- ? `topic:${location.roomId}:${location.topicId}`
140
- : location.roomId
141
- ? `room:${location.roomId}`
142
- : location.spaceId
143
- ? `space:${location.spaceId}`
144
- : 'spaceless';
145
-
146
- if (this.unreadSummariesCache.has(cacheKey)) {
147
- return this.unreadSummariesCache.get(cacheKey)!;
148
- }
149
-
150
- let roomIds: string[] = [];
151
- let targetTopicId: string | undefined;
152
- const rooms = await this.tracker.rooms.get();
153
-
154
- if (location.topicId) {
155
- if (!location.roomId) {
156
- throw new Error("roomId is required when querying by topicId");
157
- }
158
- roomIds = [location.roomId];
159
- targetTopicId = location.topicId;
160
- } else if (location.roomId) {
161
- roomIds = [location.roomId];
162
- } else if (location.spaceId) {
163
- await this.cacheSpaceFollowedTopics(location.spaceId);
164
- roomIds = rooms.findBy('spaceId', location.spaceId).items.map(r => r.id);
165
- } else {
166
- await this.cacheSpaceFollowedTopics(null);
167
- roomIds = rooms.items.filter(r => !r.spaceId).map(r => r.id);
168
- }
169
-
170
- let mentionCount = 0;
171
- let isUnread = false;
172
-
173
- for (const roomId of roomIds) {
174
- const collection = await this.getRoomFollowedTopics(roomId);
175
-
176
- if (!collection) {
177
- continue;
178
- }
179
-
180
- for (const topic of collection.items) {
181
- if (targetTopicId && topic.location.topicId !== targetTopicId) {
182
- continue;
183
- }
184
-
185
- if (topic.isUnread) {
186
- isUnread = true;
187
- }
188
-
189
- mentionCount += (topic.mentionCount ?? 0);
190
- }
191
- }
192
-
193
- const result = { mentionCount, isUnread };
194
- this.unreadSummariesCache.set(cacheKey, result);
195
-
196
- return result;
197
- }
198
-
199
- /**
200
- * For internal use. If you want to delete the message, execute a proper command on client object.
201
- * @internal
202
- */
203
- public _deleteByTopicIds(roomId: string, ...topicIds: string[]): void {
204
- this.followedTopics.get(roomId)?.delete(...topicIds);
205
- topicIds.forEach(topicId => this.invalidateUnreadSummaries(roomId, topicId));
206
- }
207
-
208
33
  /**
209
34
  * For internal use.
210
35
  * @internal
@@ -229,187 +54,25 @@ export class MessagesManager {
229
54
  return message || null;
230
55
  }
231
56
 
232
- /**
233
- * Wyczyść cache celowo, tylko dla lokalizacji których dotyczy zmiana.
234
- */
235
- private invalidateUnreadSummaries(roomId?: string, topicId?: string): void {
236
- if (!roomId) {
237
- this.unreadSummariesCache.clear();
238
- return;
239
- }
240
-
241
- this.unreadSummariesCache.delete(`room:${roomId}`);
242
-
243
- if (topicId) {
244
- this.unreadSummariesCache.delete(`topic:${roomId}:${topicId}`);
245
- } else {
246
- for (const key of this.unreadSummariesCache.keys()) {
247
- if (key.startsWith(`topic:${roomId}:`)) {
248
- this.unreadSummariesCache.delete(key);
249
- }
250
- }
251
- }
252
-
253
- for (const key of this.unreadSummariesCache.keys()) {
254
- if (key.startsWith('space:') || key === 'spaceless') {
255
- this.unreadSummariesCache.delete(key);
256
- }
257
- }
258
- }
259
-
260
- private invalidateUnreadSummariesForRooms(roomIds: string[]): void {
261
- const roomIdsSet = new Set(roomIds);
262
- roomIds.forEach(roomId => {
263
- this.unreadSummariesCache.delete(`room:${roomId}`);
264
- });
265
-
266
- for (const key of this.unreadSummariesCache.keys()) {
267
- if (key.startsWith('space:') || key === 'spaceless') {
268
- this.unreadSummariesCache.delete(key);
269
- continue;
270
- }
271
- if (key.startsWith('topic:')) {
272
- const parts = key.split(':');
273
- if (parts.length >= 2 && roomIdsSet.has(parts[1])) {
274
- this.unreadSummariesCache.delete(key);
275
- }
276
- }
277
- }
278
- }
279
-
280
57
  private createHistoryForNewRoom(room: Room): void {
281
58
  this.roomHistories.set([room.id, new RoomMessagesHistory(room, this.tracker)]);
282
59
  }
283
60
 
284
- private handleNewMessage(ev: NewMessage): void {
285
- void this.updateLocallyFollowedTopicOnNewMessage(ev);
286
- }
287
-
288
- private handleFollowedTopicUpdated(ev: FollowedTopicUpdated): void {
289
- this.followedTopics.get(ev.followedTopic.location.roomId)?.set(ev.followedTopic);
290
- this.invalidateUnreadSummaries(ev.followedTopic.location.roomId, ev.followedTopic.location.topicId);
291
- }
292
-
293
- private handleTopicFollowed(ev: TopicFollowed): void {
294
- this.setFollowedTopicsArray([ev.followedTopic.location.roomId], [ev.followedTopic]);
295
- this.invalidateUnreadSummaries(ev.followedTopic.location.roomId, ev.followedTopic.location.topicId);
296
- }
297
-
298
61
  private handleRoomDeleted(ev: RoomDeleted): void {
299
62
  this.roomHistories.delete(ev.id);
300
- this.clearRoomFollowedTopicsStructures(ev.id);
301
63
  }
302
64
 
303
65
  private handleRoomJoin(ev: RoomJoined): void {
304
- this.createHistoryForNewRoom(ev.room)
305
- this.clearRoomFollowedTopicsStructures(ev.room.id);
66
+ this.createHistoryForNewRoom(ev.room);
306
67
  }
307
68
 
308
69
  private handleRoomLeft(ev: RoomLeft): void {
309
70
  this.roomHistories.delete(ev.id);
310
- this.clearRoomFollowedTopicsStructures(ev.id);
311
- }
312
-
313
- private async handleNewTopic(ev: NewTopic): Promise<void> {
314
- if (this.followedTopics.has(ev.roomId)) {
315
- // Check if the new topic is followed by user
316
- // only if client asked for followed topics list before for this room
317
- const result = await this.tracker.client.send(
318
- 'GetFollowedTopics',
319
- {location: {roomId: ev.roomId, topicId: ev.topic.id}},
320
- );
321
- const followedTopic = result.data.followedTopics[0];
322
- if (followedTopic) {
323
- this.followedTopics.get(ev.roomId).set(followedTopic);
324
- this.invalidateUnreadSummaries(ev.roomId, ev.topic.id);
325
- }
326
- }
327
- }
328
-
329
- private handleTopicDeleted(ev: TopicDeleted): void {
330
- this.followedTopics.get(ev.location.roomId)?.delete(ev.location.topicId);
331
- this.invalidateUnreadSummaries(ev.location.roomId, ev.location.topicId);
332
71
  }
333
72
 
334
73
  private handleSession(ev: Session): void {
335
- this.followedTopics.deleteAll();
336
- this.followedTopicsPromises.forgetAll();
337
- this.invalidateUnreadSummaries();
338
74
  this.roomHistories.deleteAll();
339
75
  ev.state.rooms.forEach(room => this.createHistoryForNewRoom(room));
340
76
  this.deferredSession.resolve();
341
77
  }
342
-
343
- private async updateLocallyFollowedTopicOnNewMessage(ev: NewMessage): Promise<void> {
344
- const roomFollowedTopics = this.followedTopics.get(ev.message.location.roomId);
345
- const followedTopic = roomFollowedTopics?.get(ev.message.location.topicId);
346
-
347
- if (!roomFollowedTopics || !followedTopic || ev.message.type === 'Ephemeral') {
348
- // Skip if we don't follow this room or targeted topic or the message is ephemeral
349
- return;
350
- }
351
-
352
- const isMe = ev.message.author.user.id === this.tracker.me?.id;
353
-
354
- let update: Partial<FollowedTopic>;
355
-
356
- if (isMe) {
357
- // Reset missed messages count if new message is authored by me
358
- update = {missed: 0, lastAckMessageId: ev.message.id};
359
- } else if (ev.message.type === 'Text') {
360
- // ...check for mentions if it's text message...
361
- const member = await this.tracker.rooms.getMe(ev.message.location.roomId);
362
- const roleIds = [...(member.spaceMember?.roles ?? []), ...member.roles];
363
- const mentionHandlers = [
364
- ...roleIds.map(id => `<@&${id}>`),
365
- `<@${ev.message.author.user.id}>`,
366
- ];
367
- const mentionExists = mentionHandlers.some(handler => ev.message.content.includes(handler));
368
-
369
- update = {
370
- missed: followedTopic.missed === null ? null : followedTopic.missed + 1,
371
- isUnread: true,
372
- mentionCount: followedTopic.mentionCount + (mentionExists ? 1 : 0),
373
- };
374
- } else {
375
- // ...or just mark as unread.
376
- update = {
377
- missed: followedTopic.missed === null ? null : followedTopic.missed + 1,
378
- isUnread: true,
379
- };
380
- }
381
-
382
- roomFollowedTopics.set({...followedTopic, ...update});
383
- this.invalidateUnreadSummaries(ev.message.location.roomId, ev.message.location.topicId);
384
- }
385
-
386
- private setFollowedTopicsArray(roomIds: string[], followedTopics: FollowedTopic[]): void {
387
- const roomToTopics: {[roomId: string]: FollowedTopic[]} = {};
388
-
389
- // Reassign followed topics to limit collection change event emit
390
- followedTopics.forEach(followedTopic => {
391
- roomToTopics[followedTopic.location.roomId] ??= [];
392
- roomToTopics[followedTopic.location.roomId].push(followedTopic);
393
- });
394
-
395
- roomIds.forEach(roomId => {
396
- if (! this.followedTopics.has(roomId)) {
397
- this.followedTopics.set([roomId, new ObservableIndexedObjectCollection<FollowedTopic>(
398
- followedTopic => followedTopic.location.topicId
399
- )]);
400
- }
401
-
402
- if (roomToTopics[roomId]) {
403
- this.followedTopics.get(roomId).set(...roomToTopics[roomId]);
404
- }
405
- });
406
-
407
- this.invalidateUnreadSummariesForRooms(roomIds);
408
- }
409
-
410
- private clearRoomFollowedTopicsStructures(roomId: string): void {
411
- this.followedTopics.delete(roomId);
412
- this.followedTopicsPromises.forget(roomId);
413
- this.invalidateUnreadSummaries(roomId);
414
- }
415
78
  }
@@ -16,9 +16,11 @@ import {
16
16
  import {ChatStateTracker} from "./ChatStateTracker";
17
17
  import {DeferredTask, PromiseRegistry} from "./AsyncUtils";
18
18
  import {MessagesManager} from "./MessagesManager";
19
+ import {FollowedTopicsManager} from "./FollowedTopicsManager";
19
20
 
20
21
  export class RoomsManager {
21
22
  public readonly messages: MessagesManager;
23
+ public readonly followedTopics: FollowedTopicsManager;
22
24
 
23
25
  private readonly list = new ObservableIndexedObjectCollection<Room>('id');
24
26
  private readonly topics = new IndexedCollection<string, ObservableIndexedObjectCollection<Topic>>();
@@ -29,6 +31,7 @@ export class RoomsManager {
29
31
 
30
32
  public constructor(private tracker: ChatStateTracker) {
31
33
  this.messages = new MessagesManager(tracker);
34
+ this.followedTopics = new FollowedTopicsManager(tracker);
32
35
 
33
36
  this.tracker.client.on('NewMessage', ev => this.handleNewMessage(ev));
34
37
  this.tracker.client.on('MessagesRedacted', ev => this.handleMessagesRedacted(ev));
@@ -136,7 +139,7 @@ export class RoomsManager {
136
139
 
137
140
  for (const roomId of roomIds) {
138
141
  const topicIds: string[] = this.topics.get(roomId)?.items.map(topic => topic.id) ?? [];
139
- this.messages._deleteByTopicIds(roomId, ...topicIds);
142
+ this.followedTopics._deleteByTopicIds(roomId, ...topicIds);
140
143
  }
141
144
 
142
145
  this.topics.delete(...roomIds);