polfan-server-js-client 0.4.1 → 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.
@@ -1,4 +1,4 @@
1
- import { Bye, GetSession, JoinSpace, Session, SpaceJoined, Error as ErrorType, SpaceLeft, SpaceMemberJoined, SpaceMemberLeft, SpaceMemberUpdated, SpaceDeleted, SpaceMembers, SpaceRooms, NewRole, RoomDeleted, RoomJoined, RoomLeft, RoomMemberLeft, RoomMembersJoined, RoomMembers, NewRoom, NewTopic, TopicDeleted, NewMessage, GetPermissionOverwrites, GetComputedPermissions, LeaveSpace, CreateSpace, DeleteSpace, GetSpaceMembers, GetSpaceRooms, CreateRole, DeleteRole, AssignRole, DeassignRole, SetPermissionOverwrites, JoinRoom, LeaveRoom, CreateRoom, DeleteRoom, GetRoomMembers, CreateTopic, DeleteTopic, CreateMessage, Envelope, PermissionOverwrites, PermissionOverwritesUpdated, RoomMemberUpdated, UpdateRole, RoleUpdated, Ack, UserUpdated, UpdateRoom, RoomUpdated, UpdateSpace, SpaceUpdated, PermissionOverwriteTargets, GetPermissionOverwriteTargets, Owners, Ok, GetOwners, CreateOwner, RoleDeleted, FollowedTopicUpdated, TopicFollowed, FollowedTopics, FollowTopic, UpdateFollowedTopic, GetFollowedTopics, Messages, GetMessages, Topics, GetTopics, TopicUpdated, UpdateTopic, GetDiscoverableSpaces, DiscoverableSpaces, CreateEmoticon, DeleteEmoticon, GetEmoticons, Emoticons, EmoticonDeleted, NewEmoticon, Bans, GetBans, Ban, Unban, Kick, ClientData, GetClientData, SetClientData, GetRoomSummary, GetSpaceSummary, RoomSummaryEvent, SpaceSummaryEvent, UpdateSpaceMember, Relationships, RelationshipDeleted, NewRelationship, DeleteRelationship, CreateRelationship, RoomSummaryUpdated, Pong, Ping, RedactMessages, MessagesRedacted, ReportAbuse, UpdateRoomMember, GetRelationships, GetUserInfo, UserInfo, SetSessionData, SessionData, GetSessionData, SetUserData, UserData, GetUserData, UnfollowTopic, TopicUnfollowed, Invite, Uninvite, GetInvited, Invited } from "./types/src";
1
+ import { Bye, GetSession, JoinSpace, Session, SpaceJoined, Error as ErrorType, SpaceLeft, SpaceMemberJoined, SpaceMemberLeft, SpaceMemberUpdated, SpaceDeleted, SpaceMembers, SpaceRooms, NewRole, RoomDeleted, RoomJoined, RoomLeft, RoomMemberLeft, RoomMembersJoined, RoomMembers, NewRoom, NewTopic, TopicDeleted, NewMessage, GetPermissionOverwrites, GetComputedPermissions, LeaveSpace, CreateSpace, DeleteSpace, GetSpaceMembers, GetSpaceRooms, CreateRole, DeleteRole, AssignRole, DeassignRole, SetPermissionOverwrites, JoinRoom, LeaveRoom, CreateRoom, DeleteRoom, GetRoomMembers, CreateTopic, DeleteTopic, CreateMessage, Envelope, PermissionOverwrites, PermissionOverwritesUpdated, RoomMemberUpdated, UpdateRole, RoleUpdated, Ack, UserUpdated, UpdateRoom, RoomUpdated, UpdateSpace, SpaceUpdated, PermissionOverwriteTargets, GetPermissionOverwriteTargets, Owners, Ok, GetOwners, CreateOwner, RoleDeleted, FollowedTopicUpdated, TopicFollowed, FollowedTopics, FollowTopic, UpdateFollowedTopic, GetFollowedTopics, Messages, GetMessages, Topics, GetTopics, TopicUpdated, UpdateTopic, GetDiscoverableSpaces, DiscoverableSpaces, CreateEmoticon, DeleteEmoticon, GetEmoticons, Emoticons, EmoticonDeleted, NewEmoticon, Bans, GetBans, Ban, Unban, Kick, ClientData, GetClientData, SetClientData, GetRoomSummary, GetSpaceSummary, RoomSummaryEvent, SpaceSummaryEvent, UpdateSpaceMember, Relationships, RelationshipDeleted, NewRelationship, DeleteRelationship, CreateRelationship, RoomSummaryUpdated, Pong, Ping, RedactMessages, MessagesRedacted, ReportAbuse, UpdateRoomMember, GetRelationships, GetUserInfo, UserInfo, SetSessionData, SessionData, GetSessionData, SetUserData, UserData, GetUserData, UnfollowTopic, TopicUnfollowed, Invite, Uninvite, GetInvited, Invited, React, Unreact, GetReactionDetails, ReactionUpdated, Reacted, ReactionDetails } from "./types/src";
2
2
  import { EventTarget } from "./EventTarget";
3
3
  type ArrayOfPromiseResolvers = [(value: any) => void, (reason?: any) => void];
4
4
  type ExtraEventMap = Record<string, any>;
@@ -90,6 +90,9 @@ export type EventsMap = {
90
90
  Topics: Topics;
91
91
  TopicUpdated: TopicUpdated;
92
92
  MessagesRedacted: MessagesRedacted;
93
+ ReactionUpdated: ReactionUpdated;
94
+ Reacted: Reacted;
95
+ ReactionDetails: ReactionDetails;
93
96
  };
94
97
  /**
95
98
  * Map of commands and their corresponding events.
@@ -160,5 +163,8 @@ export type CommandsMap = {
160
163
  GetTopics: CommandDefinition<GetTopics, EventsMap['Topics']>;
161
164
  UpdateTopic: CommandDefinition<UpdateTopic, EventsMap['TopicUpdated']>;
162
165
  RedactMessages: CommandDefinition<RedactMessages, EventsMap['MessagesRedacted']>;
166
+ React: CommandDefinition<React, EventsMap['Reacted']>;
167
+ Unreact: CommandDefinition<Unreact, EventsMap['Reacted']>;
168
+ GetReactionDetails: CommandDefinition<GetReactionDetails, EventsMap['ReactionDetails']>;
163
169
  };
164
170
  export {};
@@ -90,6 +90,14 @@ export declare class Permissions {
90
90
  value: number;
91
91
  maxLayer: Layer;
92
92
  };
93
+ React: {
94
+ value: number;
95
+ maxLayer: Layer;
96
+ };
97
+ CreatePolls: {
98
+ value: number;
99
+ maxLayer: Layer;
100
+ };
93
101
  };
94
102
  static getNames(): (keyof typeof this.list)[];
95
103
  static getByName(name: keyof typeof this.list): PermissionDefinition | undefined;
@@ -1,4 +1,4 @@
1
- import { Message, Topic } from "../types/src";
1
+ import { Message, Topic, UserReaction } from "../types/src";
2
2
  import { ChatStateTracker } from "./ChatStateTracker";
3
3
  import { CollectionEventMap, ObservableIndexedObjectCollection } from "../IndexedObjectCollection";
4
4
  export declare enum WindowState {
@@ -94,10 +94,23 @@ export declare class TopicHistoryWindow extends TraversableRemoteCollection<Mess
94
94
  readonly WindowState: typeof WindowState;
95
95
  protected internalState: typeof TraversableRemoteCollection<Message>['prototype']['internalState'] & {
96
96
  traverseLock: boolean;
97
+ includeMyReactions: boolean;
98
+ myReactions: Record<string, UserReaction[]>;
97
99
  };
98
100
  constructor(roomId: string, topicId: string, tracker: ChatStateTracker, bindEvents?: boolean);
99
101
  createMirror(): TopicHistoryWindow;
100
102
  get isTraverseLocked(): boolean;
103
+ /**
104
+ * What the connected user has reacted with, keyed by message id. Kept apart from
105
+ * the messages, so the history itself is identical for every user.
106
+ */
107
+ get myReactions(): Readonly<Record<string, UserReaction[]>>;
108
+ /**
109
+ * Whether the history is fetched together with the reactions of the connected
110
+ * user. Turn it off only where the active state of a reaction is never rendered.
111
+ */
112
+ get includeMyReactions(): boolean;
113
+ set includeMyReactions(value: boolean);
101
114
  setTraverseLock(lock: boolean): Promise<void>;
102
115
  resetToLatest(force?: boolean): Promise<void>;
103
116
  fetchNext(): Promise<void>;
@@ -112,9 +125,21 @@ export declare class TopicHistoryWindow extends TraversableRemoteCollection<Mess
112
125
  protected fetchItemsAround(id: string): Promise<Message[] | null>;
113
126
  protected fetchItemsBefore(): Promise<Message[] | null>;
114
127
  protected fetchLatestItems(): Promise<Message[]>;
128
+ private fetchMessages;
129
+ /**
130
+ * The response is the truth for every message it covers, so a message it does not
131
+ * mention has no reaction of this user left on it.
132
+ */
133
+ private storeMyReactions;
115
134
  private getTopic;
116
135
  private getLatestMessageId;
117
136
  protected isLatestItemLoaded(): Promise<boolean>;
118
137
  private handleNewMessage;
138
+ /**
139
+ * The counter arrives as the global source of truth - only it is overwritten, and
140
+ * a reaction nobody holds any more (count 0) leaves the message.
141
+ */
142
+ private handleReactionUpdated;
143
+ private handleReacted;
119
144
  private handleMessagesRedacted;
120
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polfan-server-js-client",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "JavaScript client library for handling communication with Polfan chat server.",
5
5
  "author": "Jarosław Żak",
6
6
  "license": "MIT",
@@ -122,6 +122,12 @@ import {
122
122
  Uninvite,
123
123
  GetInvited,
124
124
  Invited,
125
+ React,
126
+ Unreact,
127
+ GetReactionDetails,
128
+ ReactionUpdated,
129
+ Reacted,
130
+ ReactionDetails,
125
131
  } from "./types/src";
126
132
  import {EventTarget} from "./EventTarget";
127
133
 
@@ -263,6 +269,9 @@ export type CommandResponse<CommandType extends keyof CommandsMap> = CommandsMap
263
269
  Topics: Topics,
264
270
  TopicUpdated: TopicUpdated,
265
271
  MessagesRedacted: MessagesRedacted,
272
+ ReactionUpdated: ReactionUpdated,
273
+ Reacted: Reacted,
274
+ ReactionDetails: ReactionDetails,
266
275
  };
267
276
 
268
277
  /**
@@ -338,4 +347,7 @@ export type CommandsMap = {
338
347
  GetTopics: CommandDefinition<GetTopics, EventsMap['Topics']>,
339
348
  UpdateTopic: CommandDefinition<UpdateTopic, EventsMap['TopicUpdated']>,
340
349
  RedactMessages: CommandDefinition<RedactMessages, EventsMap['MessagesRedacted']>,
350
+ React: CommandDefinition<React, EventsMap['Reacted']>,
351
+ Unreact: CommandDefinition<Unreact, EventsMap['Reacted']>,
352
+ GetReactionDetails: CommandDefinition<GetReactionDetails, EventsMap['ReactionDetails']>,
341
353
  }
@@ -33,6 +33,8 @@ export class Permissions {
33
33
  ChangeOwnColor: {value: 1 << 18, maxLayer: Layer.Room},
34
34
  RedactMessages: {value: 1 << 19, maxLayer: Layer.Topic},
35
35
  AddMembers: {value: 1 << 20, maxLayer: Layer.Space},
36
+ React: {value: 1 << 21, maxLayer: Layer.Topic},
37
+ CreatePolls: {value: 1 << 22, maxLayer: Layer.Topic},
36
38
  };
37
39
 
38
40
  public static getNames(): (keyof typeof this.list)[] {
@@ -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;