polfan-server-js-client 0.2.95 → 0.2.97

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