polfan-server-js-client 0.4.0 → 0.4.2

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 (40) hide show
  1. package/.idea/workspace.xml +71 -242
  2. package/build/index.cjs.js +219 -125
  3. package/build/index.cjs.js.map +1 -1
  4. package/build/index.umd.js +1 -1
  5. package/build/index.umd.js.map +1 -1
  6. package/build/types/AbstractChatClient.d.ts +7 -1
  7. package/build/types/Permissions.d.ts +8 -0
  8. package/build/types/state-tracker/TopicHistoryWindow.d.ts +26 -1
  9. package/build/types/types/src/index.d.ts +9 -1
  10. package/build/types/types/src/schemes/Message.d.ts +5 -1
  11. package/build/types/types/src/schemes/MessagePoll.d.ts +10 -0
  12. package/build/types/types/src/schemes/Reaction.d.ts +13 -0
  13. package/build/types/types/src/schemes/commands/CreateMessage.d.ts +3 -0
  14. package/build/types/types/src/schemes/commands/GetMessages.d.ts +5 -0
  15. package/build/types/types/src/schemes/commands/GetReactionDetails.d.ts +8 -0
  16. package/build/types/types/src/schemes/commands/React.d.ts +6 -0
  17. package/build/types/types/src/schemes/commands/Unreact.d.ts +6 -0
  18. package/build/types/types/src/schemes/events/Messages.d.ts +3 -0
  19. package/build/types/types/src/schemes/events/Reacted.d.ts +9 -0
  20. package/build/types/types/src/schemes/events/ReactionDetails.d.ts +11 -0
  21. package/build/types/types/src/schemes/events/ReactionUpdated.d.ts +8 -0
  22. package/package.json +43 -43
  23. package/src/AbstractChatClient.ts +12 -0
  24. package/src/Permissions.ts +2 -0
  25. package/src/index.ts +31 -31
  26. package/src/state-tracker/TopicHistoryWindow.ts +121 -42
  27. package/src/state-tracker/UsersManager.ts +51 -51
  28. package/src/types/src/index.ts +333 -313
  29. package/src/types/src/schemes/Message.ts +5 -1
  30. package/src/types/src/schemes/MessagePoll.ts +12 -0
  31. package/src/types/src/schemes/Reaction.ts +16 -0
  32. package/src/types/src/schemes/commands/CreateMessage.ts +3 -0
  33. package/src/types/src/schemes/commands/GetMessages.ts +5 -0
  34. package/src/types/src/schemes/commands/GetReactionDetails.ts +9 -0
  35. package/src/types/src/schemes/commands/React.ts +7 -0
  36. package/src/types/src/schemes/commands/Unreact.ts +7 -0
  37. package/src/types/src/schemes/events/Messages.ts +3 -0
  38. package/src/types/src/schemes/events/Reacted.ts +10 -0
  39. package/src/types/src/schemes/events/ReactionDetails.ts +12 -0
  40. package/src/types/src/schemes/events/ReactionUpdated.ts +9 -0
@@ -1,4 +1,14 @@
1
- import {Message, MessagesRedacted, NewMessage, Topic} from "../types/src";
1
+ import {
2
+ GetMessages,
3
+ Message,
4
+ MessageReaction,
5
+ ReactionUpdated,
6
+ MessagesRedacted,
7
+ NewMessage,
8
+ Reacted,
9
+ Topic,
10
+ UserReaction,
11
+ } from "../types/src";
2
12
  import {ChatStateTracker} from "./ChatStateTracker";
3
13
  import {CollectionEventMap, ObservableIndexedObjectCollection} from "../IndexedObjectCollection";
4
14
 
@@ -302,6 +312,8 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
302
312
 
303
313
  declare protected internalState: typeof TraversableRemoteCollection<Message>['prototype']['internalState'] & {
304
314
  traverseLock: boolean,
315
+ includeMyReactions: boolean,
316
+ myReactions: Record<string, UserReaction[]>,
305
317
  };
306
318
 
307
319
  public constructor(
@@ -313,10 +325,14 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
313
325
  super('id');
314
326
 
315
327
  this.internalState.traverseLock = false;
328
+ this.internalState.includeMyReactions = true;
329
+ this.internalState.myReactions = {};
316
330
 
317
331
  if (bindEvents) {
318
332
  this.tracker.client.on('NewMessage', ev => this.handleNewMessage(ev));
319
333
  this.tracker.client.on('MessagesRedacted', ev => this.handleMessagesRedacted(ev));
334
+ this.tracker.client.on('ReactionUpdated', ev => this.handleReactionUpdated(ev));
335
+ this.tracker.client.on('Reacted', ev => this.handleReacted(ev));
320
336
  }
321
337
  }
322
338
 
@@ -332,6 +348,26 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
332
348
  return this.internalState.traverseLock;
333
349
  }
334
350
 
351
+ /**
352
+ * What the connected user has reacted with, keyed by message id. Kept apart from
353
+ * the messages, so the history itself is identical for every user.
354
+ */
355
+ public get myReactions(): Readonly<Record<string, UserReaction[]>> {
356
+ return this.internalState.myReactions;
357
+ }
358
+
359
+ /**
360
+ * Whether the history is fetched together with the reactions of the connected
361
+ * user. Turn it off only where the active state of a reaction is never rendered.
362
+ */
363
+ public get includeMyReactions(): boolean {
364
+ return this.internalState.includeMyReactions;
365
+ }
366
+
367
+ public set includeMyReactions(value: boolean) {
368
+ this.internalState.includeMyReactions = value;
369
+ }
370
+
335
371
  public async setTraverseLock(lock: boolean): Promise<void> {
336
372
  this.internalState.traverseLock = lock;
337
373
 
@@ -384,70 +420,60 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
384
420
  protected async fetchItemsAfter(): Promise<Message[] | null> {
385
421
  const afterId = this.getAt(this.length - 1)?.id;
386
422
 
387
- if (! afterId) {
388
- // If there is no message to refer, fetch latest
389
- return null;
390
- }
391
-
392
- const result = await this.tracker.client.send('GetMessages', {
393
- location: {roomId: this.roomId, topicId: this.topicId},
394
- after: afterId,
395
- limit: this.internalState.fetchLimit,
396
- });
397
-
398
- if (result.error) {
399
- throw new Error(`Cannot fetch messages: ${result.error.message}`);
400
- }
401
-
402
- return result.data.messages;
423
+ // If there is no message to refer, fetch latest
424
+ return afterId ? this.fetchMessages({after: afterId}) : null;
403
425
  }
404
426
 
405
427
  protected async fetchItemsAround(id: string): Promise<Message[] | null> {
406
- const result = await this.tracker.client.send('GetMessages', {
407
- location: {roomId: this.roomId, topicId: this.topicId},
408
- around: id,
409
- limit: this.internalState.fetchLimit,
410
- });
411
-
412
- if (result.error) {
413
- throw new Error(`Cannot fetch messages: ${result.error.message}`);
414
- }
415
-
416
- return result.data.messages;
428
+ return this.fetchMessages({around: id});
417
429
  }
418
430
 
419
431
  protected async fetchItemsBefore(): Promise<Message[] | null> {
420
432
  const beforeId = this.getAt(0)?.id;
421
433
 
422
- if (! beforeId) {
423
- // If there is no message to refer, fetch latest
424
- return null;
425
- }
434
+ // If there is no message to refer, fetch latest
435
+ return beforeId ? this.fetchMessages({before: beforeId}) : null;
436
+ }
426
437
 
438
+ protected async fetchLatestItems(): Promise<Message[]> {
439
+ return this.fetchMessages({});
440
+ }
441
+
442
+ private async fetchMessages(criteria: Partial<GetMessages>): Promise<Message[]> {
427
443
  const result = await this.tracker.client.send('GetMessages', {
428
444
  location: {roomId: this.roomId, topicId: this.topicId},
429
- before: beforeId,
430
445
  limit: this.internalState.fetchLimit,
446
+ includeMyReactions: this.internalState.includeMyReactions,
447
+ ...criteria,
431
448
  });
432
449
 
433
450
  if (result.error) {
434
451
  throw new Error(`Cannot fetch messages: ${result.error.message}`);
435
452
  }
436
453
 
454
+ this.storeMyReactions(result.data.messages, result.data.myReactions);
455
+
437
456
  return result.data.messages;
438
457
  }
439
458
 
440
- protected async fetchLatestItems(): Promise<Message[]> {
441
- const result = await this.tracker.client.send('GetMessages', {
442
- location: {roomId: this.roomId, topicId: this.topicId},
443
- limit: this.internalState.fetchLimit,
444
- });
445
-
446
- if (result.error) {
447
- throw new Error(`Cannot fetch messages: ${result.error.message}`);
459
+ /**
460
+ * The response is the truth for every message it covers, so a message it does not
461
+ * mention has no reaction of this user left on it.
462
+ */
463
+ private storeMyReactions(messages: Message[], myReactions?: Record<string, UserReaction[]>): void {
464
+ if (! this.internalState.includeMyReactions) {
465
+ return;
448
466
  }
449
467
 
450
- return result.data.messages;
468
+ for (const message of messages) {
469
+ const own = myReactions?.[message.id];
470
+
471
+ if (own?.length) {
472
+ this.internalState.myReactions[message.id] = own;
473
+ } else {
474
+ delete this.internalState.myReactions[message.id];
475
+ }
476
+ }
451
477
  }
452
478
 
453
479
  private async getTopic(): Promise<Topic | undefined> {
@@ -475,6 +501,59 @@ export class TopicHistoryWindow extends TraversableRemoteCollection<
475
501
  }
476
502
  }
477
503
 
504
+ /**
505
+ * The counter arrives as the global source of truth - only it is overwritten, and
506
+ * a reaction nobody holds any more (count 0) leaves the message.
507
+ */
508
+ private handleReactionUpdated(ev: ReactionUpdated): void {
509
+ const message = this.get(ev.messageId);
510
+
511
+ if (! message) {
512
+ return;
513
+ }
514
+
515
+ const index = message.reactions.findIndex(
516
+ reaction => reaction.type === ev.reaction.type && reaction.value === ev.reaction.value);
517
+
518
+ if (index === -1 && ! ev.reaction.count) {
519
+ return;
520
+ }
521
+
522
+ const reactions: MessageReaction[] = [...message.reactions];
523
+
524
+ if (index === -1) {
525
+ reactions.push(ev.reaction);
526
+ } else if (ev.reaction.count) {
527
+ reactions[index] = ev.reaction; // In place - a pill must not jump around as its counter moves
528
+ } else {
529
+ reactions.splice(index, 1);
530
+ }
531
+
532
+ this.set({...message, reactions});
533
+ }
534
+
535
+ private handleReacted(ev: Reacted): void {
536
+ if (! this.has(ev.messageId)) {
537
+ return;
538
+ }
539
+
540
+ const {type, value, isAdded} = ev.reaction;
541
+ const own = (this.internalState.myReactions[ev.messageId] ?? [])
542
+ .filter(reaction => reaction.type !== type || reaction.value !== value);
543
+
544
+ if (isAdded) {
545
+ own.push({type, value});
546
+ }
547
+
548
+ if (own.length) {
549
+ this.internalState.myReactions[ev.messageId] = own;
550
+ } else {
551
+ delete this.internalState.myReactions[ev.messageId];
552
+ }
553
+
554
+ this.eventTarget.emit('change', {setItems: [ev.messageId]});
555
+ }
556
+
478
557
  private async handleMessagesRedacted(ev: MessagesRedacted): Promise<void> {
479
558
  if (ev.location.topicId !== this.topicId || ev.location.roomId !== this.roomId) {
480
559
  return;
@@ -1,52 +1,52 @@
1
- import {ChatStateTracker} from "./ChatStateTracker";
2
- import {ObservableIndexedObjectCollection} from "../IndexedObjectCollection";
3
- import {RoomMember, Session, SpaceMember, User} from "../types/src";
4
- import {extractUserFromMember} from "./functions";
5
- import {EventTarget} from "../EventTarget";
6
-
7
- export class UsersManager {
8
- public readonly onlineStatus = new EventTarget<{ change: User }>();
9
-
10
- private readonly users: ObservableIndexedObjectCollection<User> = new ObservableIndexedObjectCollection('id');
11
-
12
- public constructor(private tracker: ChatStateTracker) {
13
- // RoomMemberUpdated & SpaceMemberUpdated events are not contains user object
14
- tracker.client.on('UserUpdated', event => this.handleUsers([event.user]));
15
- tracker.client.on('RoomMembersJoined', event => this.handleMembers(event.members));
16
- tracker.client.on('SpaceMemberJoined', event => this.handleMembers([event.member]));
17
- tracker.client.on('SpaceMembers', event => this.handleMembers(event.members));
18
- tracker.client.on('RoomMembers', event => this.handleMembers(event.members));
19
- tracker.client.on('Messages', event => this.handleUsers(event.messages.map(message => message.author.user)));
20
- tracker.client.on('NewMessage', event => this.handleUsers([event.message.author.user]));
21
- tracker.client.on('Session', event => this.handleSession(event));
22
- }
23
-
24
- /**
25
- * Get all available (cached) user objects at once.
26
- */
27
- public async getAvailable(): Promise<ObservableIndexedObjectCollection<User>> {
28
- return this.users;
29
- }
30
-
31
- private handleMembers(members: (RoomMember | SpaceMember)[]): void {
32
- this.handleUsers(members.map(extractUserFromMember));
33
- }
34
-
35
- private handleSession(session: Session): void {
36
- // Keep the "seen users" cache across reconnects so bound user lists do
37
- // not blank out; just ensure our own user is present/updated. Stale
38
- // entries are refreshed as member/message collections refetch.
39
- this.handleUsers([session.user]);
40
- }
41
-
42
- private handleUsers(users: User[]): void {
43
- users.forEach(newUser => {
44
- const oldUser = this.users.get(newUser.id);
45
- if (oldUser && oldUser.status !== newUser.status) {
46
- this.onlineStatus.emit('change', newUser);
47
- }
48
- });
49
-
50
- this.users.set(...users);
51
- }
1
+ import {ChatStateTracker} from "./ChatStateTracker";
2
+ import {ObservableIndexedObjectCollection} from "../IndexedObjectCollection";
3
+ import {RoomMember, Session, SpaceMember, User} from "../types/src";
4
+ import {extractUserFromMember} from "./functions";
5
+ import {EventTarget} from "../EventTarget";
6
+
7
+ export class UsersManager {
8
+ public readonly onlineStatus = new EventTarget<{ change: User }>();
9
+
10
+ private readonly users: ObservableIndexedObjectCollection<User> = new ObservableIndexedObjectCollection('id');
11
+
12
+ public constructor(private tracker: ChatStateTracker) {
13
+ // RoomMemberUpdated & SpaceMemberUpdated events are not contains user object
14
+ tracker.client.on('UserUpdated', event => this.handleUsers([event.user]));
15
+ tracker.client.on('RoomMembersJoined', event => this.handleMembers(event.members));
16
+ tracker.client.on('SpaceMemberJoined', event => this.handleMembers([event.member]));
17
+ tracker.client.on('SpaceMembers', event => this.handleMembers(event.members));
18
+ tracker.client.on('RoomMembers', event => this.handleMembers(event.members));
19
+ tracker.client.on('Messages', event => this.handleUsers(event.messages.map(message => message.author.user)));
20
+ tracker.client.on('NewMessage', event => this.handleUsers([event.message.author.user]));
21
+ tracker.client.on('Session', event => this.handleSession(event));
22
+ }
23
+
24
+ /**
25
+ * Get all available (cached) user objects at once.
26
+ */
27
+ public async getAvailable(): Promise<ObservableIndexedObjectCollection<User>> {
28
+ return this.users;
29
+ }
30
+
31
+ private handleMembers(members: (RoomMember | SpaceMember)[]): void {
32
+ this.handleUsers(members.map(extractUserFromMember));
33
+ }
34
+
35
+ private handleSession(session: Session): void {
36
+ // Keep the "seen users" cache across reconnects so bound user lists do
37
+ // not blank out; just ensure our own user is present/updated. Stale
38
+ // entries are refreshed as member/message collections refetch.
39
+ this.handleUsers([session.user]);
40
+ }
41
+
42
+ private handleUsers(users: User[]): void {
43
+ users.forEach(newUser => {
44
+ const oldUser = this.users.get(newUser.id);
45
+ if (oldUser && oldUser.status !== newUser.status) {
46
+ this.onlineStatus.emit('change', newUser);
47
+ }
48
+ });
49
+
50
+ this.users.set(...users);
51
+ }
52
52
  }