polfan-server-js-client 0.2.2 → 0.2.6

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 (86) hide show
  1. package/.idea/copilot.data.migration.agent.xml +6 -0
  2. package/.idea/copilot.data.migration.ask.xml +6 -0
  3. package/.idea/copilot.data.migration.ask2agent.xml +6 -0
  4. package/.idea/copilot.data.migration.edit.xml +6 -0
  5. package/.idea/workspace.xml +376 -135
  6. package/babel.config.js +4 -5
  7. package/build/index.cjs.js +4538 -1816
  8. package/build/index.cjs.js.map +1 -1
  9. package/build/index.umd.js +1 -1
  10. package/build/index.umd.js.map +1 -1
  11. package/build/types/AbstractChatClient.d.ts +12 -2
  12. package/build/types/FilesClient.d.ts +7 -6
  13. package/build/types/IndexedObjectCollection.d.ts +4 -3
  14. package/build/types/Permissions.d.ts +4 -0
  15. package/build/types/WebSocketChatClient.d.ts +2 -0
  16. package/build/types/state-tracker/ChatStateTracker.d.ts +5 -0
  17. package/build/types/state-tracker/RelationshipsManager.d.ts +15 -0
  18. package/build/types/state-tracker/RoomMessagesHistory.d.ts +2 -0
  19. package/build/types/state-tracker/SpacesManager.d.ts +1 -0
  20. package/build/types/state-tracker/TopicHistoryWindow.d.ts +23 -5
  21. package/build/types/state-tracker/UsersManager.d.ts +3 -1
  22. package/build/types/types/src/index.d.ts +12 -3
  23. package/build/types/types/src/schemes/Emoticon.d.ts +1 -0
  24. package/build/types/types/src/schemes/Message.d.ts +1 -1
  25. package/build/types/types/src/schemes/Room.d.ts +2 -0
  26. package/build/types/types/src/schemes/RoomHistory.d.ts +5 -0
  27. package/build/types/types/src/schemes/RoomSummary.d.ts +1 -0
  28. package/build/types/types/src/schemes/SpaceSummary.d.ts +1 -0
  29. package/build/types/types/src/schemes/User.d.ts +2 -2
  30. package/build/types/types/src/schemes/UserRelationship.d.ts +6 -0
  31. package/build/types/types/src/schemes/commands/CreateMessage.d.ts +2 -0
  32. package/build/types/types/src/schemes/commands/CreateRelationship.d.ts +5 -0
  33. package/build/types/types/src/schemes/commands/CreateTopic.d.ts +5 -2
  34. package/build/types/types/src/schemes/commands/DeleteRelationship.d.ts +5 -0
  35. package/build/types/types/src/schemes/commands/GetRelationships.d.ts +2 -0
  36. package/build/types/types/src/schemes/commands/UpdateRoom.d.ts +2 -0
  37. package/build/types/types/src/schemes/commands/UpdateRoomMember.d.ts +7 -0
  38. package/build/types/types/src/schemes/commands/UpdateSpaceMember.d.ts +5 -0
  39. package/build/types/types/src/schemes/events/NewRelationship.d.ts +4 -0
  40. package/build/types/types/src/schemes/events/RelationshipDeleted.d.ts +4 -0
  41. package/build/types/types/src/schemes/events/Relationships.d.ts +4 -0
  42. package/build/types/types/src/schemes/events/RoomSummaryUpdated.d.ts +7 -0
  43. package/build/types/types/src/schemes/events/Session.d.ts +1 -0
  44. package/package.json +15 -30
  45. package/src/AbstractChatClient.ts +28 -4
  46. package/src/FilesClient.ts +26 -13
  47. package/src/IndexedObjectCollection.ts +26 -10
  48. package/src/Permissions.ts +1 -0
  49. package/src/WebSocketChatClient.ts +19 -11
  50. package/src/state-tracker/ChatStateTracker.ts +22 -6
  51. package/src/state-tracker/EmoticonsManager.ts +6 -4
  52. package/src/state-tracker/MessagesManager.ts +3 -3
  53. package/src/state-tracker/RelationshipsManager.ts +68 -0
  54. package/src/state-tracker/RoomMessagesHistory.ts +20 -3
  55. package/src/state-tracker/RoomsManager.ts +30 -7
  56. package/src/state-tracker/SpacesManager.ts +28 -1
  57. package/src/state-tracker/TopicHistoryWindow.ts +94 -23
  58. package/src/state-tracker/UsersManager.ts +16 -6
  59. package/src/types/src/index.ts +26 -5
  60. package/src/types/src/schemes/Emoticon.ts +1 -0
  61. package/src/types/src/schemes/Message.ts +1 -1
  62. package/src/types/src/schemes/Room.ts +2 -0
  63. package/src/types/src/schemes/RoomHistory.ts +6 -0
  64. package/src/types/src/schemes/RoomSummary.ts +1 -0
  65. package/src/types/src/schemes/SpaceSummary.ts +1 -0
  66. package/src/types/src/schemes/User.ts +2 -2
  67. package/src/types/src/schemes/UserRelationship.ts +8 -0
  68. package/src/types/src/schemes/commands/CreateMessage.ts +2 -0
  69. package/src/types/src/schemes/commands/CreateRelationship.ts +6 -0
  70. package/src/types/src/schemes/commands/CreateTopic.ts +6 -2
  71. package/src/types/src/schemes/commands/DeleteRelationship.ts +6 -0
  72. package/src/types/src/schemes/commands/GetRelationships.ts +3 -0
  73. package/src/types/src/schemes/commands/UpdateRoom.ts +2 -0
  74. package/src/types/src/schemes/commands/UpdateRoomMember.ts +7 -0
  75. package/src/types/src/schemes/commands/UpdateSpaceMember.ts +5 -0
  76. package/src/types/src/schemes/events/NewRelationship.ts +5 -0
  77. package/src/types/src/schemes/events/RelationshipDeleted.ts +5 -0
  78. package/src/types/src/schemes/events/Relationships.ts +5 -0
  79. package/src/types/src/schemes/events/RoomSummaryUpdated.ts +8 -0
  80. package/src/types/src/schemes/events/Session.ts +1 -0
  81. package/tests/history-window.test.ts +6 -1
  82. package/webpack.config.browser.js +2 -24
  83. package/webpack.config.node.js +2 -14
  84. package/.eslintignore +0 -0
  85. package/.eslintrc.json +0 -0
  86. package/src/types/src/schemes/commands/SetCustomNick.ts +0 -5
@@ -82,14 +82,30 @@ import {
82
82
  GetEmoticons,
83
83
  Emoticons,
84
84
  EmoticonDeleted,
85
- NewEmoticon, Bans, GetBans, Ban, Unban, Kick, ClientData, GetClientData, SetClientData,
85
+ NewEmoticon,
86
+ Bans,
87
+ GetBans,
88
+ Ban,
89
+ Unban,
90
+ Kick,
91
+ ClientData,
92
+ GetClientData,
93
+ SetClientData,
86
94
  GetRoomSummary,
87
95
  GetSpaceSummary,
88
96
  RoomSummaryEvent,
89
97
  SpaceSummaryEvent,
90
- SetCustomNick,
98
+ UpdateSpaceMember,
99
+ Relationships,
100
+ RelationshipDeleted,
101
+ NewRelationship,
102
+ DeleteRelationship,
103
+ CreateRelationship,
104
+ RoomSummaryUpdated,
91
105
  } from "./types/src/index";
92
106
  import {EventTarget} from "./EventTarget";
107
+ import {GetRelationships} from "./types/src/schemes/commands/GetRelationships";
108
+ import {UpdateRoomMember} from "./types/src/schemes/commands/UpdateRoomMember";
93
109
 
94
110
  type ArrayOfPromiseResolvers = [(value: any) => void, (reason?: any) => void];
95
111
 
@@ -139,7 +155,7 @@ export abstract class AbstractChatClient extends EventTarget {
139
155
  if (!this.awaitingResponse.has(envelope.ref)) {
140
156
  return;
141
157
  }
142
- this.awaitingResponse.get(envelope.ref)[0](error);
158
+ this.awaitingResponse.get(envelope.ref)[1](error);
143
159
  this.awaitingResponse.delete(envelope.ref);
144
160
  }
145
161
  }
@@ -165,6 +181,9 @@ export type EventsMap = {
165
181
  Emoticons: Emoticons,
166
182
  Bans: Bans,
167
183
  ClientData: ClientData,
184
+ NewRelationship: NewRelationship,
185
+ RelationshipDeleted: RelationshipDeleted,
186
+ Relationships: Relationships,
168
187
  // Space events
169
188
  DiscoverableSpaces: DiscoverableSpaces,
170
189
  SpaceJoined: SpaceJoined,
@@ -191,6 +210,7 @@ export type EventsMap = {
191
210
  RoomDeleted: RoomDeleted,
192
211
  RoomUpdated: RoomUpdated,
193
212
  RoomSummaryEvent: RoomSummaryEvent,
213
+ RoomSummaryUpdated: RoomSummaryUpdated,
194
214
  // Topic events
195
215
  NewTopic: NewTopic,
196
216
  TopicDeleted: TopicDeleted,
@@ -227,6 +247,9 @@ export type CommandsMap = {
227
247
  Kick: [Kick, EventsMap['Ok']],
228
248
  GetClientData: [GetClientData, EventsMap['ClientData']],
229
249
  SetClientData: [SetClientData, EventsMap['Ok']],
250
+ DeleteRelationship: [DeleteRelationship, EventsMap['RelationshipDeleted']],
251
+ CreateRelationship: [CreateRelationship, EventsMap['NewRelationship']],
252
+ GetRelationships: [GetRelationships, EventsMap['Relationships']],
230
253
  // Space commands
231
254
  GetDiscoverableSpaces: [GetDiscoverableSpaces, EventsMap['DiscoverableSpaces']],
232
255
  JoinSpace: [JoinSpace, EventsMap['SpaceJoined']],
@@ -242,7 +265,7 @@ export type CommandsMap = {
242
265
  AssignRole: [AssignRole, EventsMap['SpaceMemberUpdated'] | EventsMap['RoomMemberUpdated']],
243
266
  DeassignRole: [DeassignRole, EventsMap['SpaceMemberUpdated'] | EventsMap['RoomMemberUpdated']],
244
267
  GetSpaceSummary: [GetSpaceSummary, EventsMap['SpaceSummaryEvent']],
245
- SetCustomNick: [SetCustomNick, EventsMap['SpaceMemberUpdated']],
268
+ UpdateSpaceMember: [UpdateSpaceMember, EventsMap['SpaceMemberUpdated']],
246
269
  // Room commands
247
270
  JoinRoom: [JoinRoom, EventsMap['RoomJoined']],
248
271
  LeaveRoom: [LeaveRoom, EventsMap['RoomLeft']],
@@ -251,6 +274,7 @@ export type CommandsMap = {
251
274
  UpdateRoom: [UpdateRoom, EventsMap['RoomUpdated']],
252
275
  GetRoomMembers: [GetRoomMembers, EventsMap['RoomMembers']],
253
276
  GetRoomSummary: [GetRoomSummary, EventsMap['RoomSummaryEvent']],
277
+ UpdateRoomMember: [UpdateRoomMember, EventsMap['RoomMemberUpdated']],
254
278
  // Topic commands
255
279
  CreateTopic: [CreateTopic, EventsMap['NewTopic']],
256
280
  DeleteTopic: [DeleteTopic, EventsMap['TopicDeleted']],
@@ -3,28 +3,41 @@ import {AbstractRestClient, RestClientResponse} from "./AbstractRestClient";
3
3
  export interface File {
4
4
  id: string;
5
5
  url: string;
6
- original_url: string;
7
- original_name: string;
8
- mime_type: string;
6
+ name: string;
7
+ mime: string;
9
8
  size: number;
10
- image_dimensions: [number, number] | null;
9
+ width?: number;
10
+ height?: number;
11
11
  }
12
12
 
13
13
  export class FilesClient extends AbstractRestClient {
14
- protected defaultUrl: string = 'https://polfan.pl/webservice/api/files';
14
+ protected defaultUrl: string = 'https://files.devana.pl';
15
15
 
16
- public async uploadFile(file: Parameters<typeof FormData.prototype.append>[1]): Promise<RestClientResponse<File>> {
17
- const formData = new FormData();
18
- formData.append('file', file);
16
+ public async uploadFile(file: globalThis.File | Blob): Promise<RestClientResponse<File>> {
17
+ const name = encodeURIComponent((file as globalThis.File).name ?? '');
18
+ let headers = {
19
+ ...this.getAuthHeaders(),
20
+ Accept: 'application/json',
21
+ 'Content-Disposition': `attachment; filename="${name}"`,
22
+ 'Content-Length': file.size
23
+ };
19
24
 
20
- let headers = {...this.getAuthHeaders(), Accept: 'application/json'};
21
-
22
- const response = await fetch(this.defaultUrl, {method: 'POST', body: formData, headers});
25
+ const response = await fetch(this.getUrl('files'), {
26
+ method: 'POST',
27
+ body: file,
28
+ headers
29
+ });
23
30
 
24
31
  return this.convertFetchResponse<File>(response);
25
32
  }
26
33
 
27
- public async getFileMetadata(id: string): Promise<RestClientResponse<File>> {
28
- return this.send('GET', '/' + id);
34
+ public async getFileMeta(id: string): Promise<RestClientResponse<File>> {
35
+ return this.send('GET', 'files/' + id);
36
+ }
37
+
38
+ public async getFileMetaBulk(ids: string[]): Promise<RestClientResponse<File[]>> {
39
+ const searchParams = new URLSearchParams();
40
+ ids.forEach(id => searchParams.append('id[]', id));
41
+ return this.send('GET', 'files?' + searchParams);
29
42
  }
30
43
  }
@@ -2,16 +2,11 @@ import {EventTarget, ObservableInterface} from "./EventTarget";
2
2
 
3
3
  export class IndexedCollection<KeyT, ValueT> {
4
4
  protected _items: Map<KeyT, ValueT> = new Map();
5
- protected _mutationCounter: number = 0;
6
5
 
7
6
  public constructor(items: [key: KeyT, value: ValueT][] = []) {
8
7
  this.set(...items);
9
8
  }
10
9
 
11
- public get mutationCounter(): number {
12
- return this._mutationCounter;
13
- }
14
-
15
10
  public get items(): Map<KeyT, ValueT> {
16
11
  return this._items;
17
12
  }
@@ -21,7 +16,6 @@ export class IndexedCollection<KeyT, ValueT> {
21
16
  }
22
17
 
23
18
  public set(...items: [KeyT, ValueT][]): void {
24
- this._mutationCounter++;
25
19
  for (const item of items) {
26
20
  this._items.set(item[0], item[1]);
27
21
  }
@@ -58,6 +52,12 @@ export class IndexedCollection<KeyT, ValueT> {
58
52
  }
59
53
  return result;
60
54
  }
55
+
56
+ public createMirror(): IndexedCollection<KeyT, ValueT> {
57
+ const copy = new IndexedCollection<KeyT, ValueT>();
58
+ copy._items = this._items;
59
+ return copy;
60
+ }
61
61
  }
62
62
 
63
63
  export class IndexedObjectCollection<T> {
@@ -79,10 +79,6 @@ export class IndexedObjectCollection<T> {
79
79
  return this._items.length;
80
80
  }
81
81
 
82
- public get mutationCounter(): number {
83
- return this._items.mutationCounter;
84
- }
85
-
86
82
  public set(...items: T[]): void {
87
83
  this._items.set(...(items.map(item => [this.getId(item), item] as [string, T])));
88
84
  }
@@ -120,6 +116,12 @@ export class IndexedObjectCollection<T> {
120
116
  return result;
121
117
  }
122
118
 
119
+ public createMirror(): IndexedObjectCollection<T> {
120
+ const copy = new IndexedObjectCollection<T>(this.id);
121
+ copy._items = this._items;
122
+ return copy;
123
+ }
124
+
123
125
  protected getId(item: T): any {
124
126
  return typeof this.id === 'function' ? this.id(item) : item[this.id];
125
127
  }
@@ -161,6 +163,13 @@ export class ObservableIndexedCollection<KeyT, ValueT> extends IndexedCollection
161
163
  }
162
164
  }
163
165
 
166
+ public createMirror(): ObservableIndexedCollection<KeyT, ValueT> {
167
+ const copy = new ObservableIndexedCollection<KeyT, ValueT>();
168
+ copy.eventTarget = this.eventTarget;
169
+ copy._items = this._items;
170
+ return copy;
171
+ }
172
+
164
173
  public on(eventName: 'change', handler: (ev?: ObservableCollectionEvent<KeyT>) => void): this {
165
174
  this.eventTarget.on(eventName, handler);
166
175
  return this;
@@ -211,6 +220,13 @@ export class ObservableIndexedObjectCollection<T> extends IndexedObjectCollectio
211
220
  }
212
221
  }
213
222
 
223
+ public createMirror(): IndexedObjectCollection<T> {
224
+ const copy = new ObservableIndexedObjectCollection<T>(this.id);
225
+ copy.eventTarget = this.eventTarget;
226
+ copy._items = this._items;
227
+ return copy;
228
+ }
229
+
214
230
  public on(eventName: 'change', handler: (ev?: ObservableCollectionEvent<string>) => void): this {
215
231
  this.eventTarget.on(eventName, handler);
216
232
  return this;
@@ -30,6 +30,7 @@ export class Permissions {
30
30
  ManageBan: {value: 1 << 15, maxLayer: Layer.Room},
31
31
  Kick: {value: 1 << 16, maxLayer: Layer.Room},
32
32
  ChangeOwnNick: {value: 1 << 17, maxLayer: Layer.Space},
33
+ ChangeOwnColor: {value: 1 << 18, maxLayer: Layer.Room},
33
34
  };
34
35
 
35
36
  public static getNames(): (keyof typeof this.list)[] {
@@ -60,28 +60,28 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
60
60
 
61
61
  public async send<CommandType extends keyof CommandsMap>(commandType: CommandType, commandData: CommandsMap[CommandType][0]):
62
62
  Promise<CommandResult<CommandsMap[CommandType][1]>> {
63
- if (!this.ws || [this.ws.CLOSED, this.ws.CLOSING].includes(this.ws.readyState)) {
64
- throw new Error('Cannot send; close or closing connection state');
65
- }
66
-
67
63
  const envelope = this.createEnvelope<CommandsMap[CommandType][0]>(commandType, commandData);
68
64
  const promise = this.createPromiseFromCommandEnvelope<CommandType>(envelope);
69
65
 
70
- if (this.ws.readyState === this.ws.CONNECTING || !this.authenticated) {
66
+ if (this.isPendingReadyWsState()) {
71
67
  this.sendQueue.push(envelope);
72
68
  return promise;
73
69
  }
74
70
 
75
- if (this.ws.readyState !== this.ws.OPEN) {
76
- throw new Error(`Invalid websocket state=${this.ws.readyState}`);
77
- }
78
-
79
71
  this.sendEnvelope(envelope);
80
72
  return promise;
81
73
  }
82
74
 
83
75
  private sendEnvelope(envelope: Envelope): void {
84
- this.ws.send(JSON.stringify(envelope));
76
+ if (this.isReadyToSendWsState()) {
77
+ this.ws.send(JSON.stringify(envelope));
78
+ return;
79
+ }
80
+
81
+ this.handleEnvelopeSendError(
82
+ envelope,
83
+ new Error(`Cannot send; invalid websocket state=${this.ws?.readyState ?? '[no connection]'}`)
84
+ );
85
85
  }
86
86
 
87
87
  private onMessage(event: MessageEvent): void {
@@ -108,7 +108,7 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
108
108
  clearTimeout(this.connectingTimeoutId);
109
109
  const reconnect = event.code !== 1000; // Connection was closed because of error
110
110
  if (reconnect) {
111
- this.connect();
111
+ void this.connect();
112
112
  }
113
113
  this.emit(this.Event.disconnect, reconnect);
114
114
  }
@@ -129,4 +129,12 @@ export class WebSocketChatClient extends AbstractChatClient implements Observabl
129
129
  this.disconnect();
130
130
  this.emit(this.Event.error, new Error('Connection timeout'));
131
131
  }
132
+
133
+ private isPendingReadyWsState(): boolean {
134
+ return this.ws && this.ws.readyState === this.ws.CONNECTING || !this.authenticated;
135
+ }
136
+
137
+ private isReadyToSendWsState(): boolean {
138
+ return this.ws && this.ws.readyState === this.ws.OPEN && this.authenticated;
139
+ }
132
140
  }
@@ -6,38 +6,54 @@ import {PermissionsManager} from "./PermissionsManager";
6
6
  import {DeferredTask} from "./AsyncUtils";
7
7
  import {EmoticonsManager} from "./EmoticonsManager";
8
8
  import {UsersManager} from "./UsersManager";
9
+ import {RelationshipsManager} from "./RelationshipsManager";
9
10
 
10
11
  export class ChatStateTracker {
12
+ public readonly client: WebSocketChatClient;
13
+
11
14
  /**
12
15
  * State of your permissions.
13
16
  */
14
- public readonly permissions = new PermissionsManager(this);
17
+ public readonly permissions: PermissionsManager;
15
18
 
16
19
  /**
17
20
  * State of the rooms you are in.
18
21
  */
19
- public readonly rooms: RoomsManager = new RoomsManager(this);
22
+ public readonly rooms: RoomsManager;
20
23
 
21
24
  /**
22
25
  * State of the spaces you are in.
23
26
  */
24
- public readonly spaces = new SpacesManager(this);
27
+ public readonly spaces: SpacesManager;
25
28
 
26
29
  /**
27
30
  * State of the emoticons (global and space-related).
28
31
  */
29
- public readonly emoticons = new EmoticonsManager(this);
32
+ public readonly emoticons: EmoticonsManager;
30
33
 
31
34
  /**
32
35
  * Users related state.
33
36
  */
34
- public readonly users = new UsersManager(this);
37
+ public readonly users: UsersManager;
38
+
39
+ /**
40
+ * State of relationships with other users.
41
+ */
42
+ public readonly relationships: RelationshipsManager;
35
43
 
36
44
  private _me: User = null;
37
45
  private readonly deferredSession = new DeferredTask();
38
46
 
39
- public constructor(public readonly client: WebSocketChatClient) {
47
+ public constructor(client: WebSocketChatClient) {
48
+ this.client = client;
40
49
  this.client.on('Session', ev => this.handleSession(ev));
50
+
51
+ this.permissions = new PermissionsManager(this);
52
+ this.rooms = new RoomsManager(this);
53
+ this.spaces = new SpacesManager(this);
54
+ this.emoticons = new EmoticonsManager(this);
55
+ this.users = new UsersManager(this);
56
+ this.relationships = new RelationshipsManager(this);
41
57
  }
42
58
 
43
59
  public get me(): User | null {
@@ -21,18 +21,20 @@ export class EmoticonsManager {
21
21
  }
22
22
 
23
23
  public async get(spaceId?: string): Promise<ObservableIndexedObjectCollection<Emoticon>> {
24
- if (this.emoticonsPromises.notExist(spaceId)) {
24
+ const key = spaceId ?? GLOBAL_KEY;
25
+
26
+ if (this.emoticonsPromises.notExist(key)) {
25
27
  this.emoticonsPromises.registerByFunction(async () => {
26
28
  const result = await this.tracker.client.send('GetEmoticons', {spaceId});
27
29
  if (result.error) {
28
30
  throw result.error;
29
31
  }
30
32
  this.handleEmoticons(result.data);
31
- }, spaceId ?? GLOBAL_KEY);
33
+ }, key);
32
34
  }
33
35
 
34
- await this.emoticonsPromises.get(spaceId);
35
- return this.list.get(spaceId);
36
+ await this.emoticonsPromises.get(key);
37
+ return this.list.get(key);
36
38
  }
37
39
 
38
40
  private handleEmoticons(event: Emoticons): void {
@@ -8,7 +8,7 @@ import {
8
8
  RoomDeleted,
9
9
  RoomLeft,
10
10
  TopicDeleted,
11
- FollowedTopicUpdated, RoomJoined, NewTopic, Session, Room,
11
+ FollowedTopicUpdated, RoomJoined, NewTopic, Session, Room, MessageType,
12
12
  } from "../types/src";
13
13
  import {
14
14
  IndexedCollection,
@@ -210,8 +210,8 @@ export class MessagesManager {
210
210
  const roomFollowedTopics = this.followedTopics.get(ev.message.location.roomId);
211
211
  const followedTopic = roomFollowedTopics?.get(ev.message.location.topicId);
212
212
 
213
- if (! roomFollowedTopics || ! followedTopic) {
214
- // Skip if we don't follow this room or targeted topic
213
+ if (!roomFollowedTopics || !followedTopic || ev.message.type === 'Ephemeral') {
214
+ // Skip if we don't follow this room or targeted topic or the message is ephemeral
215
215
  return;
216
216
  }
217
217
 
@@ -0,0 +1,68 @@
1
+ import {ObservableIndexedObjectCollection} from "../IndexedObjectCollection";
2
+ import {
3
+ NewRelationship,
4
+ RelationshipDeleted,
5
+ Relationships,
6
+ UserRelationship,
7
+ UserRelationshipType
8
+ } from "../types/src";
9
+ import {PromiseRegistry} from "./AsyncUtils";
10
+ import {ChatStateTracker} from "./ChatStateTracker";
11
+
12
+ const getId = (refUserId: string, type: UserRelationshipType): string => `${refUserId}-${type}`;
13
+ const getIdFromRelationship = (relationship: UserRelationship): string => getId(relationship.refUser.id, relationship.type);
14
+
15
+ export class RelationshipsManager {
16
+ private relationships: ObservableIndexedObjectCollection<UserRelationship> = new ObservableIndexedObjectCollection<UserRelationship>(getIdFromRelationship);
17
+ private promises = new PromiseRegistry();
18
+
19
+ public constructor(private tracker: ChatStateTracker) {
20
+ this.tracker.client.on('Relationships', ev => this.handleRelationships(ev));
21
+ this.tracker.client.on('NewRelationship', ev => this.handleNewRelationship(ev));
22
+ this.tracker.client.on('RelationshipDeleted', ev => this.handleRelationshipDeleted(ev));
23
+ this.tracker.client.on('Session', () => this.handleSession());
24
+ }
25
+
26
+ public async get(): Promise<ObservableIndexedObjectCollection<UserRelationship>> {
27
+ if (this.promises.notExist('all')) {
28
+ this.promises.registerByFunction(async () => {
29
+ const result = await this.tracker.client.send('GetRelationships', {});
30
+ if (result.error) {
31
+ throw result.error;
32
+ }
33
+ }, 'all');
34
+ }
35
+
36
+ await this.promises.get('all');
37
+ return this.relationships;
38
+ }
39
+
40
+ public async exists(refUserId: string, type: UserRelationshipType): Promise<boolean> {
41
+ await this.get();
42
+ return this.relationships.has(getId(refUserId, type));
43
+ }
44
+
45
+ private handleRelationships(ev: Relationships): void {
46
+ this.relationships.deleteAll();
47
+ ev.relationships.forEach(relationship => {
48
+ this.relationships.set(relationship);
49
+ });
50
+ }
51
+
52
+ private handleNewRelationship(ev: NewRelationship): void {
53
+ if (this.promises.has('all')) {
54
+ this.relationships.set(ev.relationship);
55
+ }
56
+ }
57
+
58
+ private handleRelationshipDeleted(ev: RelationshipDeleted): void {
59
+ if (this.promises.has('all')) {
60
+ this.relationships.delete(getIdFromRelationship(ev.relationship));
61
+ }
62
+ }
63
+
64
+ private handleSession(): void {
65
+ this.promises.forgetAll();
66
+ this.relationships.deleteAll();
67
+ }
68
+ }
@@ -5,6 +5,7 @@ import {TopicHistoryWindow} from "./TopicHistoryWindow";
5
5
 
6
6
  export class RoomMessagesHistory {
7
7
  private historyWindows = new IndexedCollection<string, TopicHistoryWindow>();
8
+ private traverseLock: boolean = false;
8
9
 
9
10
  public constructor(
10
11
  private room: Room,
@@ -14,6 +15,8 @@ export class RoomMessagesHistory {
14
15
  this.tracker.client.on('NewTopic', ev => this.handleNewTopic(ev));
15
16
  this.tracker.client.on('TopicDeleted', ev => this.handleTopicDeleted(ev));
16
17
 
18
+ this.updateTraverseLock(this.room);
19
+
17
20
  if (this.room.defaultTopic) {
18
21
  this.createHistoryWindowForTopic(this.room.defaultTopic);
19
22
  }
@@ -25,7 +28,7 @@ export class RoomMessagesHistory {
25
28
  public async getMessagesWindow(topicId: string): Promise<TopicHistoryWindow | undefined> {
26
29
  let historyWindow = this.historyWindows.get(topicId);
27
30
 
28
- if (! historyWindow) {
31
+ if (!historyWindow) {
29
32
  const topic = (await this.tracker.rooms.getTopics(this.room.id, [topicId])).get(topicId);
30
33
 
31
34
  if (topic) {
@@ -36,13 +39,19 @@ export class RoomMessagesHistory {
36
39
  return this.historyWindows.get(topicId);
37
40
  }
38
41
 
39
- private handleRoomUpdated(ev: RoomUpdated): void {
42
+ private async handleRoomUpdated(ev: RoomUpdated): Promise<void> {
40
43
  if (this.room.id === ev.room.id) {
41
44
  this.room = ev.room;
42
45
 
46
+ this.updateTraverseLock(ev.room);
47
+
43
48
  if (ev.room.defaultTopic) {
44
49
  this.createHistoryWindowForTopic(ev.room.defaultTopic);
45
50
  }
51
+
52
+ for (const [, window] of Array.from(this.historyWindows.items)) {
53
+ await window.setTraverseLock(this.traverseLock);
54
+ }
46
55
  }
47
56
  }
48
57
 
@@ -63,7 +72,11 @@ export class RoomMessagesHistory {
63
72
  return;
64
73
  }
65
74
 
66
- this.historyWindows.set([topic.id, new TopicHistoryWindow(this.room.id, topic.id, this.tracker)]);
75
+ const historyWindow = new TopicHistoryWindow(this.room.id, topic.id, this.tracker);
76
+
77
+ void historyWindow.setTraverseLock(this.traverseLock);
78
+
79
+ this.historyWindows.set([topic.id, historyWindow]);
67
80
 
68
81
  // If new topic refers to some message from this room, update other structures
69
82
  if (topic.refMessage) {
@@ -71,4 +84,8 @@ export class RoomMessagesHistory {
71
84
  refHistoryWindow?._updateMessageReference(topic);
72
85
  }
73
86
  }
87
+
88
+ private updateTraverseLock(room: Room): void {
89
+ this.traverseLock = room.history.mode === 'Ephemeral';
90
+ }
74
91
  }
@@ -174,6 +174,7 @@ export class RoomsManager {
174
174
  const newMember = ev.member;
175
175
  const user = member.spaceMember?.user ?? member.user;
176
176
 
177
+ // Preserving user object, because it's not included in event
177
178
  if (newMember.spaceMember) {
178
179
  newMember.spaceMember.user = user;
179
180
  } else {
@@ -214,7 +215,9 @@ export class RoomsManager {
214
215
  }
215
216
 
216
217
  private handleRoomUpdated(ev: RoomUpdated): void {
217
- this.list.set(ev.room);
218
+ if (this.list.has(ev.room.id)) {
219
+ this.list.set(ev.room);
220
+ }
218
221
  }
219
222
 
220
223
  private handleRoomDeleted(ev: RoomDeleted): void {
@@ -295,6 +298,7 @@ export class RoomsManager {
295
298
  }
296
299
 
297
300
  private handleUserUpdated(ev: UserUpdated): void {
301
+ // Update room members users
298
302
  this.members.items.forEach((members) => {
299
303
  const member = members.get(ev.user.id);
300
304
 
@@ -313,18 +317,37 @@ export class RoomsManager {
313
317
 
314
318
  members.set(newMember);
315
319
  });
320
+
321
+ // Update recipients users
322
+ const newRooms: Room[] = [];
323
+ this.list.items.forEach(room => {
324
+ if (room.recipients?.some(user => user.id === ev.user.id)) {
325
+ room.recipients = room.recipients.map(user => user.id === ev.user.id ? ev.user : user);
326
+ newRooms.push({...room});
327
+ }
328
+ });
329
+ this.list.set(...newRooms);
316
330
  }
317
331
 
318
332
  private handleNewMessage(ev: NewMessage): void {
319
333
  const topics = this.topics.get(ev.message.location.roomId);
320
334
  const topic = topics?.get(ev.message.location.topicId);
321
335
 
322
- if (topic) {
323
- topics.set({
324
- ...topic,
325
- messageCount: topic.messageCount + 1,
326
- lastMessage: ev.message,
327
- });
336
+ if (!topic) {
337
+ return; // No topic found, nothing to update
338
+ }
339
+
340
+ const newTopic = {
341
+ ...topic,
342
+ messageCount: topic.messageCount + 1,
343
+ lastMessage: ev.message,
344
+ };
345
+
346
+ topics.set(newTopic);
347
+ const room = this.list.get(ev.message.location.roomId);
348
+
349
+ if (room.defaultTopic?.id === ev.message.location.topicId) {
350
+ this.list.set({ ...room, defaultTopic: newTopic });
328
351
  }
329
352
  }
330
353
  }
@@ -8,6 +8,7 @@ import {
8
8
  RoleUpdated,
9
9
  RoomDeleted,
10
10
  RoomSummary,
11
+ RoomSummaryUpdated,
11
12
  RoomUpdated,
12
13
  Session,
13
14
  Space,
@@ -48,6 +49,7 @@ export class SpacesManager {
48
49
  this.tracker.client.on('SpaceMemberLeft', ev => this.handleSpaceMemberLeft(ev));
49
50
  this.tracker.client.on('SpaceMembers', ev => this.handleSpaceMembers(ev));
50
51
  this.tracker.client.on('SpaceRooms', ev => this.handleSpaceRooms(ev));
52
+ this.tracker.client.on('RoomSummaryUpdated', ev => this.handleRoomSummaryUpdated(ev));
51
53
  this.tracker.client.on('SpaceMemberUpdated', ev => this.handleSpaceMemberUpdated(ev));
52
54
  this.tracker.client.on('UserUpdated', ev => this.handleUserUpdated(ev));
53
55
  this.tracker.client.on('NewRole', ev => this.handleNewRole(ev));
@@ -229,12 +231,37 @@ export class SpacesManager {
229
231
  }
230
232
 
231
233
  private handleSpaceRooms(ev: SpaceRooms): void {
232
- if (! this.rooms.has(ev.id)) {
234
+ if (!this.rooms.has(ev.id)) {
233
235
  this.rooms.set([ev.id, new ObservableIndexedObjectCollection('id', ev.summaries)]);
234
236
  ev.summaries.forEach(summary => this.roomIdToSpaceId.set([summary.id, ev.id]));
235
237
  }
236
238
  }
237
239
 
240
+ private async handleRoomSummaryUpdated(ev: RoomSummaryUpdated): Promise<void> {
241
+ const spaceId = this.roomIdToSpaceId.get(ev.summary.id);
242
+ const summariesPromise = this.roomsPromises.get(spaceId);
243
+
244
+ /**
245
+ * Update summary only if the list was already loaded.
246
+ * RoomSummaryUpdated event has a partial summary, so we need to update the existing summary by merging it.
247
+ */
248
+ if (spaceId && summariesPromise) {
249
+ await summariesPromise;
250
+
251
+ const summaries = this.rooms.get(spaceId);
252
+ const oldSummary = summaries.get(ev.summary.id);
253
+ let newSummary: RoomSummary;
254
+
255
+ if (oldSummary) {
256
+ newSummary = {...oldSummary, ...ev.summary};
257
+ } else {
258
+ newSummary = ev.summary;
259
+ }
260
+
261
+ summaries.set(newSummary);
262
+ }
263
+ }
264
+
238
265
  private handleSpaceMemberUpdated(ev: SpaceMemberUpdated): void {
239
266
  if (this.members.has(ev.spaceId)) {
240
267
  const members = this.members.get(ev.spaceId);