@roeehrl/tinode-sdk 0.25.1-sqlite.1
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.
- package/LICENSE +201 -0
- package/README.md +47 -0
- package/package.json +76 -0
- package/src/access-mode.js +567 -0
- package/src/cbuffer.js +244 -0
- package/src/cbuffer.test.js +107 -0
- package/src/comm-error.js +14 -0
- package/src/config.js +71 -0
- package/src/connection.js +537 -0
- package/src/db.js +1021 -0
- package/src/drafty.js +2758 -0
- package/src/drafty.test.js +1600 -0
- package/src/fnd-topic.js +123 -0
- package/src/index.js +29 -0
- package/src/index.native.js +35 -0
- package/src/large-file.js +325 -0
- package/src/me-topic.js +480 -0
- package/src/meta-builder.js +283 -0
- package/src/storage-sqlite.js +1081 -0
- package/src/tinode.js +2382 -0
- package/src/topic.js +2160 -0
- package/src/utils.js +309 -0
- package/src/utils.test.js +456 -0
- package/types/index.d.ts +1227 -0
- package/umd/tinode.dev.js +6856 -0
- package/umd/tinode.dev.js.map +1 -0
- package/umd/tinode.prod.js +2 -0
- package/umd/tinode.prod.js.map +1 -0
package/src/topic.js
ADDED
|
@@ -0,0 +1,2160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Topic management.
|
|
3
|
+
*
|
|
4
|
+
* @copyright 2015-2025 Tinode LLC.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
import AccessMode from './access-mode.js';
|
|
9
|
+
import CBuffer from './cbuffer.js';
|
|
10
|
+
import CommError from './comm-error.js';
|
|
11
|
+
import * as Const from './config.js';
|
|
12
|
+
import Drafty from './drafty.js';
|
|
13
|
+
import MetaGetBuilder from './meta-builder.js';
|
|
14
|
+
import {
|
|
15
|
+
listToRanges,
|
|
16
|
+
mergeObj,
|
|
17
|
+
mergeToCache,
|
|
18
|
+
normalizeArray,
|
|
19
|
+
normalizeRanges
|
|
20
|
+
} from './utils.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Topic is a class representing a logical communication channel.
|
|
24
|
+
*/
|
|
25
|
+
export default class Topic {
|
|
26
|
+
/**
|
|
27
|
+
* @callback onData
|
|
28
|
+
* @param {Data} data - Data packet
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create topic.
|
|
33
|
+
* @param {string} name - Name of the topic to create.
|
|
34
|
+
* @param {Object=} callbacks - Object with various event callbacks.
|
|
35
|
+
* @param {onData} callbacks.onData - Callback which receives a <code>{data}</code> message.
|
|
36
|
+
* @param {callback} callbacks.onMeta - Callback which receives a <code>{meta}</code> message.
|
|
37
|
+
* @param {callback} callbacks.onPres - Callback which receives a <code>{pres}</code> message.
|
|
38
|
+
* @param {callback} callbacks.onInfo - Callback which receives an <code>{info}</code> message.
|
|
39
|
+
* @param {callback} callbacks.onMetaDesc - Callback which receives changes to topic desctioption {@link desc}.
|
|
40
|
+
* @param {callback} callbacks.onMetaSub - Called for a single subscription record change.
|
|
41
|
+
* @param {callback} callbacks.onSubsUpdated - Called after a batch of subscription changes have been recieved and cached.
|
|
42
|
+
* @param {callback} callbacks.onDeleteTopic - Called after the topic is deleted.
|
|
43
|
+
* @param {callback} callbacks.onAllMessagesReceived - Called when all requested <code>{data}</code> messages have been recived.
|
|
44
|
+
*/
|
|
45
|
+
constructor(name, callbacks) {
|
|
46
|
+
// Parent Tinode object.
|
|
47
|
+
this._tinode = null;
|
|
48
|
+
|
|
49
|
+
// Server-provided data, locally immutable.
|
|
50
|
+
// topic name
|
|
51
|
+
this.name = name;
|
|
52
|
+
// Timestamp when the topic was created.
|
|
53
|
+
this.created = null;
|
|
54
|
+
// Timestamp when the topic was last updated.
|
|
55
|
+
this.updated = null;
|
|
56
|
+
// Timestamp of the last messages
|
|
57
|
+
this.touched = new Date(0);
|
|
58
|
+
// Access mode, see AccessMode
|
|
59
|
+
this.acs = new AccessMode(null);
|
|
60
|
+
// Per-topic private data (accessible by current user only).
|
|
61
|
+
this.private = null;
|
|
62
|
+
// Per-topic public data (accessible by all users).
|
|
63
|
+
this.public = null;
|
|
64
|
+
// Per-topic system-provided data (accessible by all users).
|
|
65
|
+
this.trusted = null;
|
|
66
|
+
|
|
67
|
+
// Locally cached data
|
|
68
|
+
// Subscribed users, for tracking read/recv/msg notifications.
|
|
69
|
+
this._users = {};
|
|
70
|
+
|
|
71
|
+
// Current value of locally issued seqId, used for pending messages.
|
|
72
|
+
this._queuedSeqId = Const.LOCAL_SEQID;
|
|
73
|
+
|
|
74
|
+
// The maximum known {data.seq} value.
|
|
75
|
+
this._maxSeq = 0;
|
|
76
|
+
// The minimum known {data.seq} value.
|
|
77
|
+
this._minSeq = 0;
|
|
78
|
+
// Indicator that the last request for earlier messages returned 0.
|
|
79
|
+
this._noEarlierMsgs = false;
|
|
80
|
+
// The maximum known deletion ID.
|
|
81
|
+
this._maxDel = 0;
|
|
82
|
+
// Timer object used to send 'recv' notifications.
|
|
83
|
+
this._recvNotificationTimer = null;
|
|
84
|
+
|
|
85
|
+
// User discovery tags
|
|
86
|
+
this._tags = [];
|
|
87
|
+
// Credentials such as email or phone number.
|
|
88
|
+
this._credentials = [];
|
|
89
|
+
// Auxiliary data
|
|
90
|
+
this._aux = {};
|
|
91
|
+
|
|
92
|
+
// Message versions cache (e.g. for edited messages).
|
|
93
|
+
// Keys: original message seq ID.
|
|
94
|
+
// Values: CBuffers containing newer versions of the original message
|
|
95
|
+
// ordered by seq id.
|
|
96
|
+
this._messageVersions = {};
|
|
97
|
+
// Message cache, sorted by message seq values, from old to new.
|
|
98
|
+
this._messages = new CBuffer((a, b) => {
|
|
99
|
+
return a.seq - b.seq;
|
|
100
|
+
}, true);
|
|
101
|
+
// Boolean, true if the topic is currently live
|
|
102
|
+
this._attached = false;
|
|
103
|
+
// Timestap of the most recently updated subscription.
|
|
104
|
+
this._lastSubsUpdate = new Date(0);
|
|
105
|
+
// Topic created but not yet synced with the server. Used only during initialization.
|
|
106
|
+
this._new = true;
|
|
107
|
+
// The topic is deleted at the server, this is a local copy.
|
|
108
|
+
this._deleted = false;
|
|
109
|
+
|
|
110
|
+
// Timer used to trgger {leave} request after a delay.
|
|
111
|
+
this._delayedLeaveTimer = null;
|
|
112
|
+
|
|
113
|
+
// Callbacks
|
|
114
|
+
if (callbacks) {
|
|
115
|
+
this.onData = callbacks.onData;
|
|
116
|
+
this.onMeta = callbacks.onMeta;
|
|
117
|
+
this.onPres = callbacks.onPres;
|
|
118
|
+
this.onInfo = callbacks.onInfo;
|
|
119
|
+
// A single desc update;
|
|
120
|
+
this.onMetaDesc = callbacks.onMetaDesc;
|
|
121
|
+
// A single subscription record;
|
|
122
|
+
this.onMetaSub = callbacks.onMetaSub;
|
|
123
|
+
// All subscription records received;
|
|
124
|
+
this.onSubsUpdated = callbacks.onSubsUpdated;
|
|
125
|
+
this.onTagsUpdated = callbacks.onTagsUpdated;
|
|
126
|
+
this.onCredsUpdated = callbacks.onCredsUpdated;
|
|
127
|
+
this.onAuxUpdated = callbacks.onAuxUpdated;
|
|
128
|
+
this.onDeleteTopic = callbacks.onDeleteTopic;
|
|
129
|
+
this.onAllMessagesReceived = callbacks.onAllMessagesReceived;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Static methods.
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Determine topic type from topic's name: grp, p2p, me, fnd, sys.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} name - Name of the topic to test.
|
|
139
|
+
* @returns {string} One of <code>"me"</code>, <code>"fnd"</code>, <code>"sys"</code>, <code>"grp"</code>,
|
|
140
|
+
* <code>"p2p"</code> or <code>undefined</code>.
|
|
141
|
+
*/
|
|
142
|
+
static topicType(name) {
|
|
143
|
+
const types = {
|
|
144
|
+
'me': Const.TOPIC_ME,
|
|
145
|
+
'fnd': Const.TOPIC_FND,
|
|
146
|
+
'grp': Const.TOPIC_GRP,
|
|
147
|
+
'new': Const.TOPIC_GRP,
|
|
148
|
+
'nch': Const.TOPIC_GRP,
|
|
149
|
+
'chn': Const.TOPIC_GRP,
|
|
150
|
+
'usr': Const.TOPIC_P2P,
|
|
151
|
+
'sys': Const.TOPIC_SYS,
|
|
152
|
+
'slf': Const.TOPIC_SLF
|
|
153
|
+
};
|
|
154
|
+
return types[(typeof name == 'string') ? name.substring(0, 3) : 'xxx'];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if the given topic name is a name of a 'me' topic.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} name - Name of the topic to test.
|
|
161
|
+
* @returns {boolean} <code>true</code> if the name is a name of a 'me' topic, <code>false</code> otherwise.
|
|
162
|
+
*/
|
|
163
|
+
static isMeTopicName(name) {
|
|
164
|
+
return Topic.topicType(name) == Const.TOPIC_ME;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if the given topic name is a name of a 'slf' topic.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} name - Name of the topic to test.
|
|
171
|
+
* @returns {boolean} <code>true</code> if the name is a name of a 'slf' topic, <code>false</code> otherwise.
|
|
172
|
+
*/
|
|
173
|
+
static isSelfTopicName(name) {
|
|
174
|
+
return Topic.topicType(name) == Const.TOPIC_SLF;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if the given topic name is a name of a group topic.
|
|
179
|
+
* @static
|
|
180
|
+
*
|
|
181
|
+
* @param {string} name - Name of the topic to test.
|
|
182
|
+
* @returns {boolean} <code>true</code> if the name is a name of a group topic, <code>false</code> otherwise.
|
|
183
|
+
*/
|
|
184
|
+
static isGroupTopicName(name) {
|
|
185
|
+
return Topic.topicType(name) == Const.TOPIC_GRP;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if the given topic name is a name of a p2p topic.
|
|
190
|
+
* @static
|
|
191
|
+
*
|
|
192
|
+
* @param {string} name - Name of the topic to test.
|
|
193
|
+
* @returns {boolean} <code>true</code> if the name is a name of a p2p topic, <code>false</code> otherwise.
|
|
194
|
+
*/
|
|
195
|
+
static isP2PTopicName(name) {
|
|
196
|
+
return Topic.topicType(name) == Const.TOPIC_P2P;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if the given topic name is a name of a communication topic, i.e. P2P or group.
|
|
201
|
+
* @static
|
|
202
|
+
*
|
|
203
|
+
* @param {string} name - Name of the topic to test.
|
|
204
|
+
* @returns {boolean} <code>true</code> if the name is a name of a p2p or group topic, <code>false</code> otherwise.
|
|
205
|
+
*/
|
|
206
|
+
static isCommTopicName(name) {
|
|
207
|
+
return Topic.isP2PTopicName(name) || Topic.isGroupTopicName(name) || Topic.isSelfTopicName(name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if the topic name is a name of a new topic.
|
|
212
|
+
* @static
|
|
213
|
+
*
|
|
214
|
+
* @param {string} name - topic name to check.
|
|
215
|
+
* @returns {boolean} <code>true</code> if the name is a name of a new topic, <code>false</code> otherwise.
|
|
216
|
+
*/
|
|
217
|
+
static isNewGroupTopicName(name) {
|
|
218
|
+
return (typeof name == 'string') &&
|
|
219
|
+
(name.substring(0, 3) == Const.TOPIC_NEW || name.substring(0, 3) == Const.TOPIC_NEW_CHAN);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if the topic name is a name of a channel.
|
|
224
|
+
* @static
|
|
225
|
+
*
|
|
226
|
+
* @param {string} name - topic name to check.
|
|
227
|
+
* @returns {boolean} <code>true</code> if the name is a name of a channel, <code>false</code> otherwise.
|
|
228
|
+
*/
|
|
229
|
+
static isChannelTopicName(name) {
|
|
230
|
+
return (typeof name == 'string') &&
|
|
231
|
+
(name.substring(0, 3) == Const.TOPIC_CHAN || name.substring(0, 3) == Const.TOPIC_NEW_CHAN);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Returns true if pub is meant to replace another message (e.g. original message was edited).
|
|
235
|
+
static #isReplacementMsg(pub) {
|
|
236
|
+
return pub.head && pub.head.replace;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if the topic is subscribed.
|
|
241
|
+
* @returns {boolean} True is topic is attached/subscribed, false otherwise.
|
|
242
|
+
*/
|
|
243
|
+
isSubscribed() {
|
|
244
|
+
return this._attached;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Request topic to subscribe. Wrapper for {@link Tinode#subscribe}.
|
|
249
|
+
*
|
|
250
|
+
* @param {Tinode.GetQuery=} getParams - get query parameters.
|
|
251
|
+
* @param {Tinode.SetParams=} setParams - set parameters.
|
|
252
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to the request.
|
|
253
|
+
*/
|
|
254
|
+
subscribe(getParams, setParams) {
|
|
255
|
+
// Clear request to leave topic.
|
|
256
|
+
clearTimeout(this._delayedLeaveTimer);
|
|
257
|
+
this._delayedLeaveTimer = null;
|
|
258
|
+
|
|
259
|
+
// If the topic is already subscribed, return resolved promise
|
|
260
|
+
if (this._attached) {
|
|
261
|
+
return Promise.resolve(this);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Send subscribe message, handle async response.
|
|
265
|
+
// If topic name is explicitly provided, use it. If no name, then it's a new group topic,
|
|
266
|
+
// use "new".
|
|
267
|
+
return this._tinode.subscribe(this.name || Const.TOPIC_NEW, getParams, setParams).then(ctrl => {
|
|
268
|
+
if (ctrl.code >= 300) {
|
|
269
|
+
// Do nothing if subscription status has not changed.
|
|
270
|
+
return ctrl;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this._attached = true;
|
|
274
|
+
this._deleted = false;
|
|
275
|
+
this.acs = (ctrl.params && ctrl.params.acs) ? ctrl.params.acs : this.acs;
|
|
276
|
+
|
|
277
|
+
// Set topic name for new topics and add it to cache.
|
|
278
|
+
if (this._new) {
|
|
279
|
+
delete this._new;
|
|
280
|
+
|
|
281
|
+
if (this.name != ctrl.topic) {
|
|
282
|
+
// Name may change new123456 -> grpAbCdEf. Remove from cache under the old name.
|
|
283
|
+
this._cacheDelSelf();
|
|
284
|
+
this.name = ctrl.topic;
|
|
285
|
+
}
|
|
286
|
+
this._cachePutSelf();
|
|
287
|
+
|
|
288
|
+
this.created = ctrl.ts;
|
|
289
|
+
this.updated = ctrl.ts;
|
|
290
|
+
|
|
291
|
+
if (this.name != Const.TOPIC_ME && this.name != Const.TOPIC_FND) {
|
|
292
|
+
// Add the new topic to the list of contacts maintained by the 'me' topic.
|
|
293
|
+
const me = this._tinode.getMeTopic();
|
|
294
|
+
if (me.onMetaSub) {
|
|
295
|
+
me.onMetaSub(this);
|
|
296
|
+
}
|
|
297
|
+
if (me.onSubsUpdated) {
|
|
298
|
+
me.onSubsUpdated([this.name], 1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (setParams && setParams.desc) {
|
|
303
|
+
setParams.desc._noForwarding = true;
|
|
304
|
+
this._processMetaDesc(setParams.desc);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return ctrl;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create a draft of a message without sending it to the server.
|
|
313
|
+
* @memberof Tinode.Topic#
|
|
314
|
+
*
|
|
315
|
+
* @param {string | Object} data - Content to wrap in a draft.
|
|
316
|
+
* @param {boolean=} noEcho - If <code>true</code> server will not echo message back to originating
|
|
317
|
+
* session. Otherwise the server will send a copy of the message to sender.
|
|
318
|
+
*
|
|
319
|
+
* @returns {Object} message draft.
|
|
320
|
+
*/
|
|
321
|
+
createMessage(data, noEcho) {
|
|
322
|
+
return this._tinode.createMessage(this.name, data, noEcho);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Immediately publish data to topic. Wrapper for {@link Tinode#publish}.
|
|
327
|
+
* @memberof Tinode.Topic#
|
|
328
|
+
*
|
|
329
|
+
* @param {string | Object} data - Message to publish, either plain string or a Drafty object.
|
|
330
|
+
* @param {boolean=} noEcho - If <code>true</code> server will not echo message back to originating
|
|
331
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to the request.
|
|
332
|
+
*/
|
|
333
|
+
publish(data, noEcho) {
|
|
334
|
+
return this.publishMessage(this.createMessage(data, noEcho));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Publish message created by {@link Tinode.Topic#createMessage}.
|
|
339
|
+
* @memberof Tinode.Topic#
|
|
340
|
+
*
|
|
341
|
+
* @param {Object} pub - {data} object to publish. Must be created by {@link Tinode.Topic#createMessage}
|
|
342
|
+
*
|
|
343
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to the request.
|
|
344
|
+
*/
|
|
345
|
+
publishMessage(pub) {
|
|
346
|
+
if (!this._attached) {
|
|
347
|
+
return Promise.reject(new Error("Cannot publish on inactive topic"));
|
|
348
|
+
}
|
|
349
|
+
if (this._sending) {
|
|
350
|
+
return Promise.reject(new Error("The message is already being sent"));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Send data.
|
|
354
|
+
pub._sending = true;
|
|
355
|
+
pub._failed = false;
|
|
356
|
+
|
|
357
|
+
// Extract refereces to attachments and out of band image records.
|
|
358
|
+
let attachments = null;
|
|
359
|
+
if (Drafty.hasEntities(pub.content)) {
|
|
360
|
+
attachments = [];
|
|
361
|
+
Drafty.entities(pub.content, data => {
|
|
362
|
+
if (data) {
|
|
363
|
+
if (data.ref) {
|
|
364
|
+
attachments.push(data.ref);
|
|
365
|
+
}
|
|
366
|
+
if (data.preref) {
|
|
367
|
+
attachments.push(data.preref);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
if (attachments.length == 0) {
|
|
372
|
+
attachments = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return this._tinode.publishMessage(pub, attachments).then(ctrl => {
|
|
377
|
+
pub._sending = false;
|
|
378
|
+
pub.ts = ctrl.ts;
|
|
379
|
+
this.swapMessageId(pub, ctrl.params.seq);
|
|
380
|
+
this._maybeUpdateMessageVersionsCache(pub);
|
|
381
|
+
this._routeData(pub);
|
|
382
|
+
return ctrl;
|
|
383
|
+
}).catch(err => {
|
|
384
|
+
this._tinode.logger("WARNING: Message rejected by the server", err);
|
|
385
|
+
pub._sending = false;
|
|
386
|
+
pub._failed = true;
|
|
387
|
+
if (this.onData) {
|
|
388
|
+
this.onData();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Add message to local message cache, send to the server when the promise is resolved.
|
|
395
|
+
* If promise is null or undefined, the message will be sent immediately.
|
|
396
|
+
* The message is sent when the
|
|
397
|
+
* The message should be created by {@link Tinode.Topic#createMessage}.
|
|
398
|
+
* This is probably not the final API.
|
|
399
|
+
* @memberof Tinode.Topic#
|
|
400
|
+
*
|
|
401
|
+
* @param {Object} pub - Message to use as a draft.
|
|
402
|
+
* @param {Promise} prom - Message will be sent when this promise is resolved, discarded if rejected.
|
|
403
|
+
*
|
|
404
|
+
* @returns {Promise} derived promise.
|
|
405
|
+
*/
|
|
406
|
+
publishDraft(pub, prom) {
|
|
407
|
+
const seq = pub.seq || this._getQueuedSeqId();
|
|
408
|
+
if (!pub._noForwarding) {
|
|
409
|
+
// The 'seq', 'ts', and 'from' are added to mimic {data}. They are removed later
|
|
410
|
+
// before the message is sent.
|
|
411
|
+
pub._noForwarding = true;
|
|
412
|
+
pub.seq = seq;
|
|
413
|
+
pub.ts = new Date();
|
|
414
|
+
pub.from = this._tinode.getCurrentUserID();
|
|
415
|
+
|
|
416
|
+
// Don't need an echo message because the message is added to local cache right away.
|
|
417
|
+
pub.noecho = true;
|
|
418
|
+
// Add to cache.
|
|
419
|
+
this._messages.put(pub);
|
|
420
|
+
this._tinode._db.addMessage(pub);
|
|
421
|
+
|
|
422
|
+
if (this.onData) {
|
|
423
|
+
this.onData(pub);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// If promise is provided, send the queued message when it's resolved.
|
|
427
|
+
// If no promise is provided, create a resolved one and send immediately.
|
|
428
|
+
return (prom || Promise.resolve())
|
|
429
|
+
.then(_ => {
|
|
430
|
+
if (pub._cancelled) {
|
|
431
|
+
return {
|
|
432
|
+
code: 300,
|
|
433
|
+
text: "cancelled"
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return this.publishMessage(pub);
|
|
437
|
+
}).catch(err => {
|
|
438
|
+
this._tinode.logger("WARNING: Message draft rejected", err);
|
|
439
|
+
pub._sending = false;
|
|
440
|
+
pub._failed = true;
|
|
441
|
+
pub._fatal = err instanceof CommError ? (err.code >= 400 && err.code < 500) : false;
|
|
442
|
+
if (this.onData) {
|
|
443
|
+
this.onData();
|
|
444
|
+
}
|
|
445
|
+
// Rethrow to let caller know that the operation failed.
|
|
446
|
+
throw err;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Leave the topic, optionally unsibscribe. Leaving the topic means the topic will stop
|
|
452
|
+
* receiving updates from the server. Unsubscribing will terminate user's relationship with the topic.
|
|
453
|
+
* Wrapper for {@link Tinode#leave}.
|
|
454
|
+
* @memberof Tinode.Topic#
|
|
455
|
+
*
|
|
456
|
+
* @param {boolean=} unsub - If true, unsubscribe, otherwise just leave.
|
|
457
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to the request.
|
|
458
|
+
*/
|
|
459
|
+
leave(unsub) {
|
|
460
|
+
// It's possible to unsubscribe (unsub==true) from inactive topic.
|
|
461
|
+
if (!this._attached && !unsub) {
|
|
462
|
+
return Promise.reject(new Error("Cannot leave inactive topic"));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Send a 'leave' message, handle async response
|
|
466
|
+
return this._tinode.leave(this.name, unsub).then(ctrl => {
|
|
467
|
+
this._resetSub();
|
|
468
|
+
if (unsub) {
|
|
469
|
+
this._gone();
|
|
470
|
+
}
|
|
471
|
+
return ctrl;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Leave the topic, optionally unsibscribe after a delay. Leaving the topic means the topic will stop
|
|
477
|
+
* receiving updates from the server. Unsubscribing will terminate user's relationship with the topic.
|
|
478
|
+
* Wrapper for {@link Tinode#leave}.
|
|
479
|
+
* @memberof Tinode.Topic#
|
|
480
|
+
*
|
|
481
|
+
* @param {boolean} unsub - If true, unsubscribe, otherwise just leave.
|
|
482
|
+
* @param {number} delay - time in milliseconds to delay leave request.
|
|
483
|
+
*/
|
|
484
|
+
leaveDelayed(unsub, delay) {
|
|
485
|
+
clearTimeout(this._delayedLeaveTimer);
|
|
486
|
+
this._delayedLeaveTimer = setTimeout(_ => {
|
|
487
|
+
this._delayedLeaveTimer = null;
|
|
488
|
+
this.leave(unsub)
|
|
489
|
+
}, delay);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Request topic metadata from the local cache or from the server.
|
|
494
|
+
* @memberof Tinode.Topic#
|
|
495
|
+
*
|
|
496
|
+
* @param {Tinode.GetQuery} request parameters
|
|
497
|
+
*
|
|
498
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
499
|
+
*/
|
|
500
|
+
getMeta(params) {
|
|
501
|
+
// Send {get} message, return promise.
|
|
502
|
+
return this._tinode.getMeta(this.name, params);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Request more messages from the server. The goal is to load continous range of messages
|
|
507
|
+
* covering at least between 'min' and 'max' + one full page forward (newer = true) or backwards
|
|
508
|
+
* (newer=false).
|
|
509
|
+
* @memberof Tinode.Topic#
|
|
510
|
+
*
|
|
511
|
+
* @param {number} limit number of messages to get.
|
|
512
|
+
* @param {Array.<Range>} gaps - ranges of messages to load.
|
|
513
|
+
* @param {number} min if non-negative, request newer messages with seq >= min.
|
|
514
|
+
* @param {number} max if positive, request older messages with seq < max.
|
|
515
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
516
|
+
*/
|
|
517
|
+
getMessagesPage(limit, gaps, min, max, newer) {
|
|
518
|
+
let query = gaps ?
|
|
519
|
+
this.startMetaQuery().withDataRanges(gaps, limit) :
|
|
520
|
+
newer ?
|
|
521
|
+
this.startMetaQuery().withData(min, undefined, limit) :
|
|
522
|
+
this.startMetaQuery().withData(undefined, max, limit);
|
|
523
|
+
// First try fetching from DB, then from the server.
|
|
524
|
+
return this._loadMessages(this._tinode._db, query.extract('data'))
|
|
525
|
+
.then(count => {
|
|
526
|
+
// Recalculate missing ranges.
|
|
527
|
+
gaps = this.msgHasMoreMessages(min, max, newer);
|
|
528
|
+
if (gaps.length == 0) {
|
|
529
|
+
// All messages loaded.
|
|
530
|
+
return Promise.resolve({
|
|
531
|
+
topic: this.name,
|
|
532
|
+
code: 200,
|
|
533
|
+
params: {
|
|
534
|
+
count: count
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Reduce the count of requested messages.
|
|
540
|
+
limit -= count;
|
|
541
|
+
// Update query with new values loaded from DB.
|
|
542
|
+
query = this.startMetaQuery().withDataRanges(gaps, limit);
|
|
543
|
+
return this.getMeta(query.build());
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Request to get pinned messages from the local cache or from the server.
|
|
549
|
+
* @memberof Tinode.Topic#
|
|
550
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
551
|
+
*/
|
|
552
|
+
getPinnedMessages() {
|
|
553
|
+
const pins = this.aux('pins');
|
|
554
|
+
if (!Array.isArray(pins)) {
|
|
555
|
+
return Promise.resolve(0);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const loaded = [];
|
|
559
|
+
let remains = pins;
|
|
560
|
+
// First try fetching from DB, then check deleted log, then ask the server.
|
|
561
|
+
return this._tinode._db.readMessages(this.name, {
|
|
562
|
+
ranges: listToRanges(remains)
|
|
563
|
+
})
|
|
564
|
+
.then(msgs => {
|
|
565
|
+
msgs.forEach(data => {
|
|
566
|
+
// The 'data' could be undefined.
|
|
567
|
+
if (data) {
|
|
568
|
+
loaded.push(data.seq);
|
|
569
|
+
this._messages.put(data);
|
|
570
|
+
this._maybeUpdateMessageVersionsCache(data);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
if (loaded.length < pins.length) {
|
|
574
|
+
// Some messages are missing, try dellog.
|
|
575
|
+
remains = pins.filter(seq => !loaded.includes(seq));
|
|
576
|
+
return this._tinode._db.readMessages(this.name, {
|
|
577
|
+
ranges: listToRanges(remains)
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
})
|
|
582
|
+
.then(ranges => {
|
|
583
|
+
if (ranges) {
|
|
584
|
+
// Found some deleted ranges in dellog.
|
|
585
|
+
remains.forEach(seq => {
|
|
586
|
+
if (ranges.find(r => r.low <= seq && r.hi > seq)) {
|
|
587
|
+
loaded.push(seq);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (loaded.length == pins.length) {
|
|
592
|
+
// Got all pinned messages from the local cache.
|
|
593
|
+
return Promise.resolve({
|
|
594
|
+
topic: this.name,
|
|
595
|
+
code: 200,
|
|
596
|
+
params: {
|
|
597
|
+
count: loaded.length
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
remains = pins.filter(seq => !loaded.includes(seq));
|
|
603
|
+
return this.getMeta(this.startMetaQuery().withDataList(remains).build());
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Update topic metadata.
|
|
609
|
+
* @memberof Tinode.Topic#
|
|
610
|
+
*
|
|
611
|
+
* @param {Tinode.SetParams} params parameters to update.
|
|
612
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
613
|
+
*/
|
|
614
|
+
setMeta(params) {
|
|
615
|
+
if (params.tags) {
|
|
616
|
+
params.tags = normalizeArray(params.tags);
|
|
617
|
+
}
|
|
618
|
+
// Send Set message, handle async response.
|
|
619
|
+
return this._tinode.setMeta(this.name, params)
|
|
620
|
+
.then(ctrl => {
|
|
621
|
+
if (ctrl && ctrl.code >= 300) {
|
|
622
|
+
// Not modified
|
|
623
|
+
return ctrl;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (params.sub) {
|
|
627
|
+
params.sub.topic = this.name;
|
|
628
|
+
if (ctrl.params && ctrl.params.acs) {
|
|
629
|
+
params.sub.acs = ctrl.params.acs;
|
|
630
|
+
params.sub.updated = ctrl.ts;
|
|
631
|
+
}
|
|
632
|
+
if (!params.sub.user) {
|
|
633
|
+
// This is a subscription update of the current user.
|
|
634
|
+
// Assign user ID otherwise the update will be ignored by _processMetaSubs.
|
|
635
|
+
params.sub.user = this._tinode.getCurrentUserID();
|
|
636
|
+
if (!params.desc) {
|
|
637
|
+
// Force update to topic's asc.
|
|
638
|
+
params.desc = {};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
params.sub._noForwarding = true;
|
|
642
|
+
this._processMetaSubs([params.sub]);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (params.desc) {
|
|
646
|
+
if (ctrl.params && ctrl.params.acs) {
|
|
647
|
+
params.desc.acs = ctrl.params.acs;
|
|
648
|
+
params.desc.updated = ctrl.ts;
|
|
649
|
+
}
|
|
650
|
+
this._processMetaDesc(params.desc);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (params.tags) {
|
|
654
|
+
this._processMetaTags(params.tags);
|
|
655
|
+
}
|
|
656
|
+
if (params.cred) {
|
|
657
|
+
this._processMetaCreds([params.cred], true);
|
|
658
|
+
}
|
|
659
|
+
if (params.aux) {
|
|
660
|
+
this._processMetaAux(params.aux);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return ctrl;
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Update access mode of the current user or of another topic subsriber.
|
|
668
|
+
* @memberof Tinode.Topic#
|
|
669
|
+
*
|
|
670
|
+
* @param {string} uid - UID of the user to update or null to update current user.
|
|
671
|
+
* @param {string} update - the update value, full or delta.
|
|
672
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
673
|
+
*/
|
|
674
|
+
updateMode(uid, update) {
|
|
675
|
+
const user = uid ? this.subscriber(uid) : null;
|
|
676
|
+
const am = user ?
|
|
677
|
+
user.acs.updateGiven(update).getGiven() :
|
|
678
|
+
this.getAccessMode().updateWant(update).getWant();
|
|
679
|
+
|
|
680
|
+
return this.setMeta({
|
|
681
|
+
sub: {
|
|
682
|
+
user: uid,
|
|
683
|
+
mode: am
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Create new topic subscription. Wrapper for {@link Tinode#setMeta}.
|
|
689
|
+
* @memberof Tinode.Topic#
|
|
690
|
+
*
|
|
691
|
+
* @param {string} uid - ID of the user to invite
|
|
692
|
+
* @param {string=} mode - Access mode. <code>null</code> means to use default.
|
|
693
|
+
*
|
|
694
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
695
|
+
*/
|
|
696
|
+
invite(uid, mode) {
|
|
697
|
+
return this.setMeta({
|
|
698
|
+
sub: {
|
|
699
|
+
user: uid,
|
|
700
|
+
mode: mode
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Archive or un-archive the topic. Wrapper for {@link Tinode#setMeta}.
|
|
706
|
+
* @memberof Tinode.Topic#
|
|
707
|
+
*
|
|
708
|
+
* @param {boolean} arch - true to archive the topic, false otherwise.
|
|
709
|
+
*
|
|
710
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
711
|
+
*/
|
|
712
|
+
archive(arch) {
|
|
713
|
+
if (this.private && (!this.private.arch == !arch)) {
|
|
714
|
+
return Promise.resolve(arch);
|
|
715
|
+
}
|
|
716
|
+
return this.setMeta({
|
|
717
|
+
desc: {
|
|
718
|
+
private: {
|
|
719
|
+
arch: arch ? true : Const.DEL_CHAR
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Set message as pinned or unpinned by adding it to aux.pins array. Wrapper for {@link Tinode#setMeta}.
|
|
726
|
+
* @memberof Tinode.Topic#
|
|
727
|
+
*
|
|
728
|
+
* @param {number} seq - seq ID of the message to pin or un-pin.
|
|
729
|
+
* @param {boolean} pin - true to pin the message, false to un-pin.
|
|
730
|
+
*
|
|
731
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
732
|
+
*/
|
|
733
|
+
pinMessage(seq, pin) {
|
|
734
|
+
let pinned = this.aux('pins');
|
|
735
|
+
if (!Array.isArray(pinned)) {
|
|
736
|
+
pinned = [];
|
|
737
|
+
}
|
|
738
|
+
let changed = false;
|
|
739
|
+
if (pin) {
|
|
740
|
+
if (!pinned.includes(seq)) {
|
|
741
|
+
changed = true;
|
|
742
|
+
if (pinned.length == Const.MAX_PINNED_COUNT) {
|
|
743
|
+
pinned.shift();
|
|
744
|
+
}
|
|
745
|
+
pinned.push(seq);
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
if (pinned.includes(seq)) {
|
|
749
|
+
changed = true;
|
|
750
|
+
pinned = pinned.filter(id => id != seq);
|
|
751
|
+
if (pinned.length == 0) {
|
|
752
|
+
pinned = Const.DEL_CHAR;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (changed) {
|
|
757
|
+
return this.setMeta({
|
|
758
|
+
aux: {
|
|
759
|
+
pins: pinned
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
return Promise.resolve();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Pin topic to the top of the contact list.
|
|
768
|
+
* @memberof Tinode.Topic#
|
|
769
|
+
*
|
|
770
|
+
* @param {string} topic - Name of the topic to pin.
|
|
771
|
+
* @param {boolean} [pin=false] - If true, pin the topic, otherwise unpin.
|
|
772
|
+
*
|
|
773
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
774
|
+
*/
|
|
775
|
+
pinTopic(topic, pin) {
|
|
776
|
+
// Unsupported operation for non-me topics.
|
|
777
|
+
return Promise.reject(new Error("Pinning topics is not supported here"));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Get the rank of the pinned topic.
|
|
782
|
+
* @memberof Tinode.Topic#
|
|
783
|
+
* @param {string} topic - Name of the topic to check.
|
|
784
|
+
*
|
|
785
|
+
* @returns {number} numeric rank of the pinned topic in the range 1..N (N being the top,
|
|
786
|
+
* N - the number of pinned topics) or 0 if not pinned.
|
|
787
|
+
*/
|
|
788
|
+
pinnedTopicRank(topic) {
|
|
789
|
+
// Unsupported for non-me topics.
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Delete messages. Hard-deleting messages requires Deleter (D) permission.
|
|
795
|
+
* Wrapper for {@link Tinode#delMessages}.
|
|
796
|
+
* @memberof Tinode.Topic#
|
|
797
|
+
*
|
|
798
|
+
* @param {Array.<Tinode.SeqRange>} ranges - Ranges of message IDs to delete.
|
|
799
|
+
* @param {boolean=} hard - Hard or soft delete
|
|
800
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
801
|
+
*/
|
|
802
|
+
delMessages(ranges, hard) {
|
|
803
|
+
if (!this._attached) {
|
|
804
|
+
return Promise.reject(new Error("Cannot delete messages in inactive topic"));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const tosend = normalizeRanges(ranges, this._maxSeq)
|
|
808
|
+
|
|
809
|
+
// Send {del} message, return promise
|
|
810
|
+
let result;
|
|
811
|
+
if (tosend.length > 0) {
|
|
812
|
+
result = this._tinode.delMessages(this.name, tosend, hard);
|
|
813
|
+
} else {
|
|
814
|
+
result = Promise.resolve({
|
|
815
|
+
params: {
|
|
816
|
+
del: 0
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
// Update local cache.
|
|
821
|
+
return result.then(ctrl => {
|
|
822
|
+
if (ctrl.params.del > this._maxDel) {
|
|
823
|
+
this._maxDel = Math.max(ctrl.params.del, this._maxDel);
|
|
824
|
+
this.clear = Math.max(ctrl.params.del, this.clear);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
ranges.forEach(rec => {
|
|
828
|
+
if (rec.hi) {
|
|
829
|
+
this.flushMessageRange(rec.low, rec.hi);
|
|
830
|
+
} else {
|
|
831
|
+
this.flushMessage(rec.low);
|
|
832
|
+
}
|
|
833
|
+
this._messages.put({
|
|
834
|
+
seq: rec.low,
|
|
835
|
+
low: rec.low,
|
|
836
|
+
hi: rec.hi,
|
|
837
|
+
_deleted: true
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// Make a record.
|
|
842
|
+
this._tinode._db.addDelLog(this.name, ctrl.params.del, ranges);
|
|
843
|
+
|
|
844
|
+
if (this.onData) {
|
|
845
|
+
// Calling with no parameters to indicate the messages were deleted.
|
|
846
|
+
this.onData();
|
|
847
|
+
}
|
|
848
|
+
return ctrl;
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Delete all messages. Hard-deleting messages requires Deleter permission.
|
|
853
|
+
* @memberof Tinode.Topic#
|
|
854
|
+
*
|
|
855
|
+
* @param {boolean} hardDel - true if messages should be hard-deleted.
|
|
856
|
+
*
|
|
857
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
858
|
+
*/
|
|
859
|
+
delMessagesAll(hardDel) {
|
|
860
|
+
if (!this._maxSeq || this._maxSeq <= 0) {
|
|
861
|
+
// There are no messages to delete.
|
|
862
|
+
return Promise.resolve();
|
|
863
|
+
}
|
|
864
|
+
return this.delMessages([{
|
|
865
|
+
low: 1,
|
|
866
|
+
hi: this._maxSeq + 1,
|
|
867
|
+
_all: true
|
|
868
|
+
}], hardDel);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Delete multiple messages defined by their IDs. Hard-deleting messages requires Deleter permission.
|
|
873
|
+
* @memberof Tinode.Topic#
|
|
874
|
+
*
|
|
875
|
+
* @param {Array.<number>} list - list of seq IDs to delete.
|
|
876
|
+
* @param {boolean=} hardDel - true if messages should be hard-deleted.
|
|
877
|
+
*
|
|
878
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
879
|
+
*/
|
|
880
|
+
delMessagesList(list, hardDel) {
|
|
881
|
+
// Send {del} message, return promise
|
|
882
|
+
return this.delMessages(listToRanges(list), hardDel);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Delete original message and edited variants. Hard-deleting messages requires Deleter permission.
|
|
887
|
+
* @memberof Tinode.Topic#
|
|
888
|
+
*
|
|
889
|
+
* @param {number} seq - original seq ID of the message to delete.
|
|
890
|
+
* @param {boolean=} hardDel - true if messages should be hard-deleted.
|
|
891
|
+
*
|
|
892
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to the request.
|
|
893
|
+
*/
|
|
894
|
+
delMessagesEdits(seq, hardDel) {
|
|
895
|
+
const list = [seq];
|
|
896
|
+
this.messageVersions(seq, msg => list.push(msg.seq));
|
|
897
|
+
// Send {del} message, return promise
|
|
898
|
+
return this.delMessagesList(list, hardDel);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Delete topic. Requires Owner permission. Wrapper for {@link Tinode#delTopic}.
|
|
903
|
+
* @memberof Tinode.Topic#
|
|
904
|
+
*
|
|
905
|
+
* @param {boolean} hard - had-delete topic.
|
|
906
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to the request.
|
|
907
|
+
*/
|
|
908
|
+
delTopic(hard) {
|
|
909
|
+
if (this._deleted) {
|
|
910
|
+
// The topic is already deleted at the server, just remove from DB.
|
|
911
|
+
this._gone();
|
|
912
|
+
return Promise.resolve(null);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return this._tinode.delTopic(this.name, hard).then(ctrl => {
|
|
916
|
+
this._deleted = true;
|
|
917
|
+
this._resetSub();
|
|
918
|
+
this._gone();
|
|
919
|
+
return ctrl;
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Delete subscription. Requires Share permission. Wrapper for {@link Tinode#delSubscription}.
|
|
924
|
+
* @memberof Tinode.Topic#
|
|
925
|
+
*
|
|
926
|
+
* @param {string} user - ID of the user to remove subscription for.
|
|
927
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
928
|
+
*/
|
|
929
|
+
delSubscription(user) {
|
|
930
|
+
if (!this._attached) {
|
|
931
|
+
return Promise.reject(new Error("Cannot delete subscription in inactive topic"));
|
|
932
|
+
}
|
|
933
|
+
// Send {del} message, return promise
|
|
934
|
+
return this._tinode.delSubscription(this.name, user).then(ctrl => {
|
|
935
|
+
// Remove the object from the subscription cache;
|
|
936
|
+
delete this._users[user];
|
|
937
|
+
// Notify listeners
|
|
938
|
+
if (this.onSubsUpdated) {
|
|
939
|
+
this.onSubsUpdated(Object.keys(this._users));
|
|
940
|
+
}
|
|
941
|
+
return ctrl;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Send a read/recv notification.
|
|
946
|
+
* @memberof Tinode.Topic#
|
|
947
|
+
*
|
|
948
|
+
* @param {string} what - what notification to send: <code>recv</code>, <code>read</code>.
|
|
949
|
+
* @param {number} seq - ID or the message read or received.
|
|
950
|
+
*/
|
|
951
|
+
note(what, seq) {
|
|
952
|
+
if (!this._attached) {
|
|
953
|
+
// Cannot sending {note} on an inactive topic".
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Update local cache with the new count.
|
|
958
|
+
const user = this._users[this._tinode.getCurrentUserID()];
|
|
959
|
+
let update = false;
|
|
960
|
+
if (user) {
|
|
961
|
+
// Self-subscription is found.
|
|
962
|
+
if (!user[what] || user[what] < seq) {
|
|
963
|
+
user[what] = seq;
|
|
964
|
+
update = true;
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
// Self-subscription is not found.
|
|
968
|
+
update = (this[what] | 0) < seq;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (update) {
|
|
972
|
+
// Send notification to the server.
|
|
973
|
+
this._tinode.note(this.name, what, seq);
|
|
974
|
+
// Update locally cached contact with the new count.
|
|
975
|
+
this._updateMyReadRecv(what, seq);
|
|
976
|
+
|
|
977
|
+
if (this.acs != null && !this.acs.isMuted()) {
|
|
978
|
+
const me = this._tinode.getMeTopic();
|
|
979
|
+
// Sent a notification to 'me' listeners.
|
|
980
|
+
me._refreshContact(what, this);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Send a 'recv' receipt. Wrapper for {@link Tinode#noteRecv}.
|
|
987
|
+
* @memberof Tinode.Topic#
|
|
988
|
+
*
|
|
989
|
+
* @param {number} seq - ID of the message to aknowledge.
|
|
990
|
+
*/
|
|
991
|
+
noteRecv(seq) {
|
|
992
|
+
this.note('recv', seq);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Send a 'read' receipt. Wrapper for {@link Tinode#noteRead}.
|
|
996
|
+
* @memberof Tinode.Topic#
|
|
997
|
+
*
|
|
998
|
+
* @param {number} seq - ID of the message to aknowledge or 0/undefined to acknowledge the latest messages.
|
|
999
|
+
*/
|
|
1000
|
+
noteRead(seq) {
|
|
1001
|
+
seq = seq || this._maxSeq;
|
|
1002
|
+
if (seq > 0) {
|
|
1003
|
+
this.note('read', seq);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Send a key-press notification. Wrapper for {@link Tinode#noteKeyPress}.
|
|
1008
|
+
* @memberof Tinode.Topic#
|
|
1009
|
+
*/
|
|
1010
|
+
noteKeyPress() {
|
|
1011
|
+
if (this._attached) {
|
|
1012
|
+
this._tinode.noteKeyPress(this.name);
|
|
1013
|
+
} else {
|
|
1014
|
+
this._tinode.logger("INFO: Cannot send notification in inactive topic");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Send a notification than a video or audio message is . Wrapper for {@link Tinode#noteKeyPress}.
|
|
1019
|
+
* @memberof Tinode.Topic#
|
|
1020
|
+
* @param audioOnly - true if the recording is audio-only, false if it's a video recording.
|
|
1021
|
+
*/
|
|
1022
|
+
noteRecording(audioOnly) {
|
|
1023
|
+
if (this._attached) {
|
|
1024
|
+
this._tinode.noteKeyPress(this.name, audioOnly ? 'kpa' : 'kpv');
|
|
1025
|
+
} else {
|
|
1026
|
+
this._tinode.logger("INFO: Cannot send notification in inactive topic");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Send a {note what='call'}. Wrapper for {@link Tinode#videoCall}.
|
|
1032
|
+
* @memberof Tinode#
|
|
1033
|
+
*
|
|
1034
|
+
* @param {string} evt - Call event.
|
|
1035
|
+
* @param {int} seq - ID of the call message the event pertains to.
|
|
1036
|
+
* @param {string} payload - Payload associated with this event (e.g. SDP string).
|
|
1037
|
+
*
|
|
1038
|
+
* @returns {Promise} Promise (for some call events) which will
|
|
1039
|
+
* be resolved/rejected on receiving server reply
|
|
1040
|
+
*/
|
|
1041
|
+
videoCall(evt, seq, payload) {
|
|
1042
|
+
if (!this._attached && !['ringing', 'hang-up'].includes(evt)) {
|
|
1043
|
+
// Cannot {call} on an inactive topic".
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
return this._tinode.videoCall(this.name, seq, evt, payload);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Update cached read/recv/unread counts for the current user.
|
|
1050
|
+
_updateMyReadRecv(what, seq, ts) {
|
|
1051
|
+
let oldVal, doUpdate = false;
|
|
1052
|
+
|
|
1053
|
+
seq = seq | 0;
|
|
1054
|
+
this.seq = this.seq | 0;
|
|
1055
|
+
this.read = this.read | 0;
|
|
1056
|
+
this.recv = this.recv | 0;
|
|
1057
|
+
switch (what) {
|
|
1058
|
+
case 'recv':
|
|
1059
|
+
oldVal = this.recv;
|
|
1060
|
+
this.recv = Math.max(this.recv, seq);
|
|
1061
|
+
doUpdate = (oldVal != this.recv);
|
|
1062
|
+
break;
|
|
1063
|
+
case 'read':
|
|
1064
|
+
oldVal = this.read;
|
|
1065
|
+
this.read = Math.max(this.read, seq);
|
|
1066
|
+
doUpdate = (oldVal != this.read);
|
|
1067
|
+
break;
|
|
1068
|
+
case 'msg':
|
|
1069
|
+
oldVal = this.seq;
|
|
1070
|
+
this.seq = Math.max(this.seq, seq);
|
|
1071
|
+
if (!this.touched || this.touched < ts) {
|
|
1072
|
+
this.touched = ts;
|
|
1073
|
+
}
|
|
1074
|
+
doUpdate = (oldVal != this.seq);
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Sanity checks.
|
|
1079
|
+
if (this.recv < this.read) {
|
|
1080
|
+
this.recv = this.read;
|
|
1081
|
+
doUpdate = true;
|
|
1082
|
+
}
|
|
1083
|
+
if (this.seq < this.recv) {
|
|
1084
|
+
this.seq = this.recv;
|
|
1085
|
+
if (!this.touched || this.touched < ts) {
|
|
1086
|
+
this.touched = ts;
|
|
1087
|
+
}
|
|
1088
|
+
doUpdate = true;
|
|
1089
|
+
}
|
|
1090
|
+
this.unread = this.seq - this.read;
|
|
1091
|
+
return doUpdate;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Get user description from global cache. The user does not need to be a
|
|
1095
|
+
* subscriber of this topic.
|
|
1096
|
+
* @memberof Tinode.Topic#
|
|
1097
|
+
*
|
|
1098
|
+
* @param {string} uid - ID of the user to fetch.
|
|
1099
|
+
* @return {Object} user description or undefined.
|
|
1100
|
+
*/
|
|
1101
|
+
userDesc(uid) {
|
|
1102
|
+
// TODO: handle asynchronous requests
|
|
1103
|
+
const user = this._cacheGetUser(uid);
|
|
1104
|
+
if (user) {
|
|
1105
|
+
return user; // Promise.resolve(user)
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Get description of the p2p peer from subscription cache.
|
|
1110
|
+
* @memberof Tinode.Topic#
|
|
1111
|
+
*
|
|
1112
|
+
* @return {Object} peer's description or undefined.
|
|
1113
|
+
*/
|
|
1114
|
+
p2pPeerDesc() {
|
|
1115
|
+
if (!this.isP2PType()) {
|
|
1116
|
+
return undefined;
|
|
1117
|
+
}
|
|
1118
|
+
return this._users[this.name];
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Iterate over cached subscribers. If callback is undefined, use this.onMetaSub.
|
|
1122
|
+
* @memberof Tinode.Topic#
|
|
1123
|
+
*
|
|
1124
|
+
* @param {function} callback - Callback which will receive subscribers one by one.
|
|
1125
|
+
* @param {Object=} context - Value of `this` inside the `callback`.
|
|
1126
|
+
*/
|
|
1127
|
+
subscribers(callback, context) {
|
|
1128
|
+
const cb = (callback || this.onMetaSub);
|
|
1129
|
+
if (cb) {
|
|
1130
|
+
for (let idx in this._users) {
|
|
1131
|
+
cb.call(context, this._users[idx], idx, this._users);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Get a copy of cached tags.
|
|
1137
|
+
* @memberof Tinode.Topic#
|
|
1138
|
+
*
|
|
1139
|
+
* @return {Array.<string>} a copy of tags
|
|
1140
|
+
*/
|
|
1141
|
+
tags() {
|
|
1142
|
+
// Return a copy.
|
|
1143
|
+
return this._tags.slice(0);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Get auxiliary entry by key.
|
|
1147
|
+
* @memberof Tinode.Topic#
|
|
1148
|
+
* @param {string} key - auxiliary data key to retrieve.
|
|
1149
|
+
* @return {Object} value for the <code>key</code> or <code>undefined</code>.
|
|
1150
|
+
*/
|
|
1151
|
+
aux(key) {
|
|
1152
|
+
return this._aux[key];
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Get alias value (unique tag with alias: prefix), if present.
|
|
1156
|
+
* The prefix is stripped off.
|
|
1157
|
+
* @memberof Tinode.Topic#
|
|
1158
|
+
* @return {string} alias or <code>undefined</code>.
|
|
1159
|
+
*/
|
|
1160
|
+
alias() {
|
|
1161
|
+
const alias = this._tags && this._tags.find(t => t.startsWith(Const.TAG_ALIAS));
|
|
1162
|
+
if (!alias) {
|
|
1163
|
+
return undefined;
|
|
1164
|
+
}
|
|
1165
|
+
// Remove 'alias:' prefix.
|
|
1166
|
+
return alias.substring(Const.TAG_ALIAS.length);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Get cached subscription for the given user ID.
|
|
1171
|
+
* @memberof Tinode.Topic#
|
|
1172
|
+
*
|
|
1173
|
+
* @param {string} uid - id of the user to query for
|
|
1174
|
+
* @return user description or undefined.
|
|
1175
|
+
*/
|
|
1176
|
+
subscriber(uid) {
|
|
1177
|
+
return this._users[uid];
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Iterate over versions of a message: call <code>callback</code> for each version (excluding original).
|
|
1181
|
+
* If <code>callback</code> is undefined, does nothing.
|
|
1182
|
+
* @memberof Tinode.Topic#
|
|
1183
|
+
*
|
|
1184
|
+
* @param {number} origSeq - seq ID of the original message.
|
|
1185
|
+
* @param {Tinode.ForEachCallbackType} callback - Callback which will receive messages one by one. See {@link Tinode.CBuffer#forEach}
|
|
1186
|
+
* @param {Object} context - Value of `this` inside the `callback`.
|
|
1187
|
+
*/
|
|
1188
|
+
messageVersions(origSeq, callback, context) {
|
|
1189
|
+
if (!callback) {
|
|
1190
|
+
// No callback? We are done then.
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const versions = this._messageVersions[origSeq];
|
|
1194
|
+
if (!versions) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
versions.forEach(callback, undefined, undefined, context);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Iterate over cached messages: call <code>callback</code> for each message in the range [sinceIdx, beforeIdx).
|
|
1201
|
+
* If <code>callback</code> is undefined, use <code>this.onData</code>.
|
|
1202
|
+
* @memberof Tinode.Topic#
|
|
1203
|
+
*
|
|
1204
|
+
* @param {Tinode.ForEachCallbackType} callback - Callback which will receive messages one by one. See {@link Tinode.CBuffer#forEach}
|
|
1205
|
+
* @param {number} sinceId - Optional seqId to start iterating from (inclusive).
|
|
1206
|
+
* @param {number} beforeId - Optional seqId to stop iterating before it is reached (exclusive).
|
|
1207
|
+
* @param {Object} context - Value of `this` inside the `callback`.
|
|
1208
|
+
*/
|
|
1209
|
+
messages(callback, sinceId, beforeId, context) {
|
|
1210
|
+
const cb = (callback || this.onData);
|
|
1211
|
+
if (cb) {
|
|
1212
|
+
const startIdx = typeof sinceId == 'number' ? this._messages.find({
|
|
1213
|
+
seq: sinceId
|
|
1214
|
+
}, true) : undefined;
|
|
1215
|
+
const beforeIdx = typeof beforeId == 'number' ? this._messages.find({
|
|
1216
|
+
seq: beforeId
|
|
1217
|
+
}, true) : undefined;
|
|
1218
|
+
if (startIdx != -1 && beforeIdx != -1) {
|
|
1219
|
+
// Step 1. Filter out all replacement messages and
|
|
1220
|
+
// save displayable messages in a temporary buffer.
|
|
1221
|
+
let msgs = [];
|
|
1222
|
+
this._messages.forEach((msg, unused1, unused2, i) => {
|
|
1223
|
+
if (Topic.#isReplacementMsg(msg)) {
|
|
1224
|
+
// Skip replacements.
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (msg._deleted) {
|
|
1228
|
+
// Skip deleted ranges.
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
// In case the massage was edited, replace timestamp of the version with the original's timestamp.
|
|
1232
|
+
const latest = this.latestMsgVersion(msg.seq) || msg;
|
|
1233
|
+
if (!latest._origTs) {
|
|
1234
|
+
latest._origTs = latest.ts;
|
|
1235
|
+
latest._origSeq = latest.seq;
|
|
1236
|
+
latest.ts = msg.ts;
|
|
1237
|
+
latest.seq = msg.seq;
|
|
1238
|
+
}
|
|
1239
|
+
msgs.push({
|
|
1240
|
+
data: latest,
|
|
1241
|
+
idx: i
|
|
1242
|
+
});
|
|
1243
|
+
}, startIdx, beforeIdx, {});
|
|
1244
|
+
// Step 2. Loop over displayble messages invoking cb on each of them.
|
|
1245
|
+
msgs.forEach((val, i) => {
|
|
1246
|
+
cb.call(context, val.data,
|
|
1247
|
+
(i > 0 ? msgs[i - 1].data : undefined),
|
|
1248
|
+
(i < msgs.length - 1 ? msgs[i + 1].data : undefined), val.idx);
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Get the message from cache by literal <code>seq</code> (does not resolve message edits).
|
|
1255
|
+
* @memberof Tinode.Topic#
|
|
1256
|
+
*
|
|
1257
|
+
* @param {number} seq - message seqId to search for.
|
|
1258
|
+
* @returns {Object} the message with the given <code>seq</code> or <code>undefined</code>, if no such message is found.
|
|
1259
|
+
*/
|
|
1260
|
+
findMessage(seq) {
|
|
1261
|
+
const idx = this._messages.find({
|
|
1262
|
+
seq: seq
|
|
1263
|
+
});
|
|
1264
|
+
if (idx >= 0) {
|
|
1265
|
+
return this._messages.getAt(idx);
|
|
1266
|
+
}
|
|
1267
|
+
return undefined;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Get the most recent non-deleted message from cache.
|
|
1271
|
+
* @memberof Tinode.Topic#
|
|
1272
|
+
*
|
|
1273
|
+
* @returns {Object} the most recent cached message or <code>undefined</code>, if no messages are cached.
|
|
1274
|
+
*/
|
|
1275
|
+
latestMessage() {
|
|
1276
|
+
return this._messages.getLast(msg => !msg._deleted);
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Get the latest version for message.
|
|
1280
|
+
* @memberof Tinode.Topic#
|
|
1281
|
+
*
|
|
1282
|
+
* @param {number} seq - original seq ID of the message.
|
|
1283
|
+
* @returns {Object} the latest version of the message or null if message not found.
|
|
1284
|
+
*/
|
|
1285
|
+
latestMsgVersion(seq) {
|
|
1286
|
+
const versions = this._messageVersions[seq];
|
|
1287
|
+
return versions ? versions.getLast() : null;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Get the maximum cached seq ID.
|
|
1291
|
+
* @memberof Tinode.Topic#
|
|
1292
|
+
*
|
|
1293
|
+
* @returns {number} the greatest seq ID in cache.
|
|
1294
|
+
*/
|
|
1295
|
+
maxMsgSeq() {
|
|
1296
|
+
return this._maxSeq;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Get the minimum cached seq ID.
|
|
1300
|
+
* @memberof Tinode.Topic#
|
|
1301
|
+
*
|
|
1302
|
+
* @returns {number} the smallest seq ID in cache or 0.
|
|
1303
|
+
*/
|
|
1304
|
+
minMsgSeq() {
|
|
1305
|
+
return this._minSeq;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Get the maximum deletion ID.
|
|
1309
|
+
* @memberof Tinode.Topic#
|
|
1310
|
+
*
|
|
1311
|
+
* @returns {number} the greatest deletion ID.
|
|
1312
|
+
*/
|
|
1313
|
+
maxClearId() {
|
|
1314
|
+
return this._maxDel;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Get the number of messages in the cache.
|
|
1318
|
+
* @memberof Tinode.Topic#
|
|
1319
|
+
*
|
|
1320
|
+
* @returns {number} count of cached messages.
|
|
1321
|
+
*/
|
|
1322
|
+
messageCount() {
|
|
1323
|
+
return this._messages.length();
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Iterate over cached unsent messages. Wraps {@link Tinode.Topic#messages}.
|
|
1327
|
+
* @memberof Tinode.Topic#
|
|
1328
|
+
*
|
|
1329
|
+
* @param {function} callback - Callback which will receive messages one by one. See {@link Tinode.CBuffer#forEach}
|
|
1330
|
+
* @param {Object} context - Value of <code>this</code> inside the <code>callback</code>.
|
|
1331
|
+
*/
|
|
1332
|
+
queuedMessages(callback, context) {
|
|
1333
|
+
if (!callback) {
|
|
1334
|
+
throw new Error("Callback must be provided");
|
|
1335
|
+
}
|
|
1336
|
+
this.messages(callback, Const.LOCAL_SEQID, undefined, context);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Get the number of topic subscribers who marked this message as either recv or read
|
|
1340
|
+
* Current user is excluded from the count.
|
|
1341
|
+
* @memberof Tinode.Topic#
|
|
1342
|
+
*
|
|
1343
|
+
* @param {string} what - what action to consider: received <code>"recv"</code> or read <code>"read"</code>.
|
|
1344
|
+
* @param {number} seq - ID or the message read or received.
|
|
1345
|
+
*
|
|
1346
|
+
* @returns {number} the number of subscribers who marked the message with the given ID as read or received.
|
|
1347
|
+
*/
|
|
1348
|
+
msgReceiptCount(what, seq) {
|
|
1349
|
+
let count = 0;
|
|
1350
|
+
if (seq > 0) {
|
|
1351
|
+
const me = this._tinode.getCurrentUserID();
|
|
1352
|
+
for (let idx in this._users) {
|
|
1353
|
+
const user = this._users[idx];
|
|
1354
|
+
if (user.user !== me && user[what] >= seq) {
|
|
1355
|
+
count++;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return count;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Get the number of topic subscribers who marked this message (and all older messages) as read.
|
|
1363
|
+
* The current user is excluded from the count.
|
|
1364
|
+
* @memberof Tinode.Topic#
|
|
1365
|
+
*
|
|
1366
|
+
* @param {number} seq - message id to check.
|
|
1367
|
+
* @returns {number} number of subscribers who claim to have received the message.
|
|
1368
|
+
*/
|
|
1369
|
+
msgReadCount(seq) {
|
|
1370
|
+
return this.msgReceiptCount('read', seq);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Get the number of topic subscribers who marked this message (and all older messages) as received.
|
|
1374
|
+
* The current user is excluded from the count.
|
|
1375
|
+
* @memberof Tinode.Topic#
|
|
1376
|
+
*
|
|
1377
|
+
* @param {number} seq - Message id to check.
|
|
1378
|
+
* @returns {number} Number of subscribers who claim to have received the message.
|
|
1379
|
+
*/
|
|
1380
|
+
msgRecvCount(seq) {
|
|
1381
|
+
return this.msgReceiptCount('recv', seq);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Check if cached message IDs indicate that the server may have more messages.
|
|
1385
|
+
* @memberof Tinode.Topic#
|
|
1386
|
+
*
|
|
1387
|
+
* @param {number} min - smallest seq ID loaded range.
|
|
1388
|
+
* @param {number} max - greatest seq ID in loaded range.
|
|
1389
|
+
* @param {boolean} newer - if <code>true</code>, check for newer messages only.
|
|
1390
|
+
* @returns {Array.<Range>} - missing ranges in the selected direction.
|
|
1391
|
+
*/
|
|
1392
|
+
msgHasMoreMessages(min, max, newer) {
|
|
1393
|
+
// Find gaps in cached messages.
|
|
1394
|
+
const gaps = [];
|
|
1395
|
+
if (min >= max) {
|
|
1396
|
+
return gaps;
|
|
1397
|
+
}
|
|
1398
|
+
let maxSeq = 0;
|
|
1399
|
+
let gap;
|
|
1400
|
+
this._messages.forEach((msg, prev) => {
|
|
1401
|
+
const p = prev || {
|
|
1402
|
+
seq: 0
|
|
1403
|
+
};
|
|
1404
|
+
const expected = p._deleted ? p.hi : p.seq + 1;
|
|
1405
|
+
if (msg.seq > expected) {
|
|
1406
|
+
gap = {
|
|
1407
|
+
low: expected,
|
|
1408
|
+
hi: msg.seq
|
|
1409
|
+
};
|
|
1410
|
+
} else {
|
|
1411
|
+
gap = null;
|
|
1412
|
+
}
|
|
1413
|
+
// If newer: collect all gaps from min to infinity.
|
|
1414
|
+
// If older: collect all gaps from max to zero.
|
|
1415
|
+
if (gap && (newer ? gap.hi >= min : gap.low < max)) {
|
|
1416
|
+
gaps.push(gap);
|
|
1417
|
+
}
|
|
1418
|
+
maxSeq = expected;
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
if (maxSeq < this.seq) {
|
|
1422
|
+
gap = {
|
|
1423
|
+
low: maxSeq + 1,
|
|
1424
|
+
hi: this.seq + 1
|
|
1425
|
+
};
|
|
1426
|
+
if (newer ? gap.hi >= min : gap.low < max) {
|
|
1427
|
+
gaps.push(gap);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return gaps;
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Check if the given seq Id is id of the most recent message.
|
|
1434
|
+
* @memberof Tinode.Topic#
|
|
1435
|
+
*
|
|
1436
|
+
* @param {number} seqId id of the message to check
|
|
1437
|
+
*/
|
|
1438
|
+
isNewMessage(seqId) {
|
|
1439
|
+
return this._maxSeq <= seqId;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Remove one message from local cache.
|
|
1443
|
+
* @memberof Tinode.Topic#
|
|
1444
|
+
*
|
|
1445
|
+
* @param {number} seqId id of the message to remove from cache.
|
|
1446
|
+
* @returns {Message} removed message or undefined if such message was not found.
|
|
1447
|
+
*/
|
|
1448
|
+
flushMessage(seqId) {
|
|
1449
|
+
const idx = this._messages.find({
|
|
1450
|
+
seq: seqId
|
|
1451
|
+
});
|
|
1452
|
+
delete this._messageVersions[seqId];
|
|
1453
|
+
if (idx >= 0) {
|
|
1454
|
+
this._tinode._db.remMessages(this.name, seqId);
|
|
1455
|
+
return this._messages.delAt(idx);
|
|
1456
|
+
}
|
|
1457
|
+
return undefined;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Remove a range of messages from the local cache.
|
|
1461
|
+
* @memberof Tinode.Topic#
|
|
1462
|
+
*
|
|
1463
|
+
* @param {number} fromId seq ID of the first message to remove (inclusive).
|
|
1464
|
+
* @param {number} untilId seqID of the last message to remove (exclusive).
|
|
1465
|
+
*
|
|
1466
|
+
* @returns {Message[]} array of removed messages (could be empty).
|
|
1467
|
+
*/
|
|
1468
|
+
flushMessageRange(fromId, untilId) {
|
|
1469
|
+
// Remove range from persistent cache.
|
|
1470
|
+
this._tinode._db.remMessages(this.name, fromId, untilId);
|
|
1471
|
+
|
|
1472
|
+
// Remove all versions keyed by IDs in the range.
|
|
1473
|
+
for (let i = fromId; i < untilId; i++) {
|
|
1474
|
+
delete this._messageVersions[i];
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// start, end: find insertion points (nearest == true).
|
|
1478
|
+
const since = this._messages.find({
|
|
1479
|
+
seq: fromId
|
|
1480
|
+
}, true);
|
|
1481
|
+
return since >= 0 ? this._messages.delRange(since, this._messages.find({
|
|
1482
|
+
seq: untilId
|
|
1483
|
+
}, true)) : [];
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Update message's seqId.
|
|
1487
|
+
* @memberof Tinode.Topic#
|
|
1488
|
+
*
|
|
1489
|
+
* @param {Object} pub message object.
|
|
1490
|
+
* @param {number} newSeqId new seq id for pub.
|
|
1491
|
+
*/
|
|
1492
|
+
swapMessageId(pub, newSeqId) {
|
|
1493
|
+
const idx = this._messages.find(pub);
|
|
1494
|
+
const numMessages = this._messages.length();
|
|
1495
|
+
if (0 <= idx && idx < numMessages) {
|
|
1496
|
+
// Remove message with the old seq ID.
|
|
1497
|
+
this._messages.delAt(idx);
|
|
1498
|
+
this._tinode._db.remMessages(this.name, pub.seq);
|
|
1499
|
+
// Add message with the new seq ID.
|
|
1500
|
+
pub.seq = newSeqId;
|
|
1501
|
+
this._messages.put(pub);
|
|
1502
|
+
this._tinode._db.addMessage(pub);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Attempt to stop message from being sent.
|
|
1507
|
+
* @memberof Tinode.Topic#
|
|
1508
|
+
*
|
|
1509
|
+
* @param {number} seqId id of the message to stop sending and remove from cache.
|
|
1510
|
+
*
|
|
1511
|
+
* @returns {boolean} <code>true</code> if message was cancelled, <code>false</code> otherwise.
|
|
1512
|
+
*/
|
|
1513
|
+
cancelSend(seqId) {
|
|
1514
|
+
const idx = this._messages.find({
|
|
1515
|
+
seq: seqId
|
|
1516
|
+
});
|
|
1517
|
+
if (idx >= 0) {
|
|
1518
|
+
const msg = this._messages.getAt(idx);
|
|
1519
|
+
const status = this.msgStatus(msg);
|
|
1520
|
+
if (status == Const.MESSAGE_STATUS_QUEUED ||
|
|
1521
|
+
status == Const.MESSAGE_STATUS_FAILED ||
|
|
1522
|
+
status == Const.MESSAGE_STATUS_FATAL) {
|
|
1523
|
+
this._tinode._db.remMessages(this.name, seqId);
|
|
1524
|
+
msg._cancelled = true;
|
|
1525
|
+
this._messages.delAt(idx);
|
|
1526
|
+
if (this.onData) {
|
|
1527
|
+
// Calling with no parameters to indicate the message was deleted.
|
|
1528
|
+
this.onData();
|
|
1529
|
+
}
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Get type of the topic: me, p2p, grp, fnd...
|
|
1537
|
+
* @memberof Tinode.Topic#
|
|
1538
|
+
*
|
|
1539
|
+
* @returns {string} One of 'me', 'p2p', 'grp', 'fnd', 'sys' or <code>undefined</code>.
|
|
1540
|
+
*/
|
|
1541
|
+
getType() {
|
|
1542
|
+
return Topic.topicType(this.name);
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Get current user's access mode of the topic.
|
|
1546
|
+
* @memberof Tinode.Topic#
|
|
1547
|
+
*
|
|
1548
|
+
* @returns {Tinode.AccessMode} - user's access mode
|
|
1549
|
+
*/
|
|
1550
|
+
getAccessMode() {
|
|
1551
|
+
return this.acs;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Set current user's access mode of the topic.
|
|
1555
|
+
* @memberof Tinode.Topic#
|
|
1556
|
+
*
|
|
1557
|
+
* @param {AccessMode | Object} acs - access mode to set.
|
|
1558
|
+
*/
|
|
1559
|
+
setAccessMode(acs) {
|
|
1560
|
+
return this.acs = new AccessMode(acs);
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Get topic's default access mode.
|
|
1564
|
+
* @memberof Tinode.Topic#
|
|
1565
|
+
*
|
|
1566
|
+
* @returns {Tinode.DefAcs} - access mode, such as {auth: `RWP`, anon: `N`}.
|
|
1567
|
+
*/
|
|
1568
|
+
getDefaultAccess() {
|
|
1569
|
+
return this.defacs;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Initialize new meta {@link Tinode.GetQuery} builder. The query is attched to the current topic.
|
|
1573
|
+
* It will not work correctly if used with a different topic.
|
|
1574
|
+
* @memberof Tinode.Topic#
|
|
1575
|
+
*
|
|
1576
|
+
* @returns {Tinode.MetaGetBuilder} query attached to the current topic.
|
|
1577
|
+
*/
|
|
1578
|
+
startMetaQuery() {
|
|
1579
|
+
return new MetaGetBuilder(this);
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Check if topic is archived, i.e. private.arch == true.
|
|
1583
|
+
* @memberof Tinode.Topic#
|
|
1584
|
+
*
|
|
1585
|
+
* @returns {boolean} - <code>true</code> if topic is archived, <code>false</code> otherwise.
|
|
1586
|
+
*/
|
|
1587
|
+
isArchived() {
|
|
1588
|
+
return this.private && !!this.private.arch;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Check if topic is a 'me' topic.
|
|
1592
|
+
* @memberof Tinode.Topic#
|
|
1593
|
+
*
|
|
1594
|
+
* @returns {boolean} - <code>true</code> if topic is a 'me' topic, <code>false</code> otherwise.
|
|
1595
|
+
*/
|
|
1596
|
+
isMeType() {
|
|
1597
|
+
return Topic.isMeTopicName(this.name);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Check if topic is a 'slf' topic.
|
|
1601
|
+
* @memberof Tinode.Topic#
|
|
1602
|
+
*
|
|
1603
|
+
* @returns {boolean} - <code>true</code> if topic is a 'slf' topic, <code>false</code> otherwise.
|
|
1604
|
+
*/
|
|
1605
|
+
isSelfType() {
|
|
1606
|
+
return Topic.isSelfTopicName(this.name);
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Check if topic is a channel.
|
|
1610
|
+
* @memberof Tinode.Topic#
|
|
1611
|
+
*
|
|
1612
|
+
* @returns {boolean} - <code>true</code> if topic is a channel, <code>false</code> otherwise.
|
|
1613
|
+
*/
|
|
1614
|
+
isChannelType() {
|
|
1615
|
+
return Topic.isChannelTopicName(this.name);
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Check if topic is a group topic.
|
|
1619
|
+
* @memberof Tinode.Topic#
|
|
1620
|
+
*
|
|
1621
|
+
* @returns {boolean} - <code>true</code> if topic is a group, <code>false</code> otherwise.
|
|
1622
|
+
*/
|
|
1623
|
+
isGroupType() {
|
|
1624
|
+
return Topic.isGroupTopicName(this.name);
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Check if topic is a p2p topic.
|
|
1628
|
+
* @memberof Tinode.Topic#
|
|
1629
|
+
*
|
|
1630
|
+
* @returns {boolean} - <code>true</code> if topic is a p2p topic, <code>false</code> otherwise.
|
|
1631
|
+
*/
|
|
1632
|
+
isP2PType() {
|
|
1633
|
+
return Topic.isP2PTopicName(this.name);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Check if topic is a communication topic, i.e. a group or p2p topic.
|
|
1637
|
+
* @memberof Tinode.Topic#
|
|
1638
|
+
*
|
|
1639
|
+
* @returns {boolean} - <code>true</code> if topic is a p2p or group topic, <code>false</code> otherwise.
|
|
1640
|
+
*/
|
|
1641
|
+
isCommType() {
|
|
1642
|
+
return Topic.isCommTopicName(this.name);
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Get status (queued, sent, received etc) of a given message in the context
|
|
1646
|
+
* of this topic.
|
|
1647
|
+
* @memberof Tinode.Topic#
|
|
1648
|
+
*
|
|
1649
|
+
* @param {Message} msg - message to check for status.
|
|
1650
|
+
* @param {boolean} upd - update chached message status.
|
|
1651
|
+
*
|
|
1652
|
+
* @returns message status constant.
|
|
1653
|
+
*/
|
|
1654
|
+
msgStatus(msg, upd) {
|
|
1655
|
+
let status = Const.MESSAGE_STATUS_NONE;
|
|
1656
|
+
if (this._tinode.isMe(msg.from)) {
|
|
1657
|
+
if (msg._sending) {
|
|
1658
|
+
status = Const.MESSAGE_STATUS_SENDING;
|
|
1659
|
+
} else if (msg._fatal || msg._cancelled) {
|
|
1660
|
+
status = Const.MESSAGE_STATUS_FATAL;
|
|
1661
|
+
} else if (msg._failed) {
|
|
1662
|
+
status = Const.MESSAGE_STATUS_FAILED;
|
|
1663
|
+
} else if (msg.seq >= Const.LOCAL_SEQID) {
|
|
1664
|
+
status = Const.MESSAGE_STATUS_QUEUED;
|
|
1665
|
+
} else if (this.msgReadCount(msg.seq) > 0) {
|
|
1666
|
+
status = Const.MESSAGE_STATUS_READ;
|
|
1667
|
+
} else if (this.msgRecvCount(msg.seq) > 0) {
|
|
1668
|
+
status = Const.MESSAGE_STATUS_RECEIVED;
|
|
1669
|
+
} else if (msg.seq > 0) {
|
|
1670
|
+
status = Const.MESSAGE_STATUS_SENT;
|
|
1671
|
+
}
|
|
1672
|
+
} else {
|
|
1673
|
+
status = Const.MESSAGE_STATUS_TO_ME;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (upd && msg._status != status) {
|
|
1677
|
+
msg._status = status;
|
|
1678
|
+
this._tinode._db.updMessageStatus(this.name, msg.seq, status);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
return status;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// If msg is a replacement for another message, save the msg in the message versions cache
|
|
1685
|
+
// as a newer version for the message it's supposed to replace.
|
|
1686
|
+
_maybeUpdateMessageVersionsCache(msg) {
|
|
1687
|
+
if (!Topic.#isReplacementMsg(msg)) {
|
|
1688
|
+
// Check if this message is the original in the chain of edits and if so
|
|
1689
|
+
// ensure all version have the same sender.
|
|
1690
|
+
if (this._messageVersions[msg.seq]) {
|
|
1691
|
+
// Remove versions with different 'from'.
|
|
1692
|
+
this._messageVersions[msg.seq].filter(version => version.from == msg.from);
|
|
1693
|
+
if (this._messageVersions[msg.seq].isEmpty()) {
|
|
1694
|
+
delete this._messageVersions[msg.seq];
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const targetSeq = parseInt(msg.head.replace.split(':')[1]);
|
|
1701
|
+
if (targetSeq > msg.seq) {
|
|
1702
|
+
// Substitutes are supposed to have higher seq ids.
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const targetMsg = this.findMessage(targetSeq);
|
|
1706
|
+
if (targetMsg && targetMsg.from != msg.from) {
|
|
1707
|
+
// Substitute cannot change the sender.
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
const versions = this._messageVersions[targetSeq] || new CBuffer((a, b) => {
|
|
1711
|
+
return a.seq - b.seq;
|
|
1712
|
+
}, true);
|
|
1713
|
+
versions.put(msg);
|
|
1714
|
+
this._messageVersions[targetSeq] = versions;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Process data message
|
|
1718
|
+
_routeData(data) {
|
|
1719
|
+
if (data.content) {
|
|
1720
|
+
if (!this.touched || this.touched < data.ts) {
|
|
1721
|
+
this.touched = data.ts;
|
|
1722
|
+
this._tinode._db.updTopic(this);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
if (data.seq > this._maxSeq) {
|
|
1727
|
+
this._maxSeq = data.seq;
|
|
1728
|
+
this.msgStatus(data, true);
|
|
1729
|
+
// Ackn receiving the message.
|
|
1730
|
+
clearTimeout(this._recvNotificationTimer);
|
|
1731
|
+
this._recvNotificationTimer = setTimeout(_ => {
|
|
1732
|
+
this._recvNotificationTimer = null;
|
|
1733
|
+
this.noteRecv(this._maxSeq);
|
|
1734
|
+
}, Const.RECV_TIMEOUT);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
if (data.seq < this._minSeq || this._minSeq == 0) {
|
|
1738
|
+
this._minSeq = data.seq;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const outgoing = ((!this.isChannelType() && !data.from) || this._tinode.isMe(data.from));
|
|
1742
|
+
|
|
1743
|
+
if (data.head && data.head.webrtc && data.head.mime == Drafty.getContentType() && data.content) {
|
|
1744
|
+
// Rewrite VC body with info from the headers.
|
|
1745
|
+
const upd = {
|
|
1746
|
+
state: data.head.webrtc,
|
|
1747
|
+
duration: data.head['webrtc-duration'],
|
|
1748
|
+
incoming: !outgoing,
|
|
1749
|
+
};
|
|
1750
|
+
if (data.head.vc) {
|
|
1751
|
+
upd.vc = true;
|
|
1752
|
+
}
|
|
1753
|
+
data.content = Drafty.updateVideoCall(data.content, upd);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
if (!data._noForwarding) {
|
|
1757
|
+
this._messages.put(data);
|
|
1758
|
+
this._tinode._db.addMessage(data);
|
|
1759
|
+
this._maybeUpdateMessageVersionsCache(data);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (this.onData) {
|
|
1763
|
+
this.onData(data);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Update locally cached contact with the new message count.
|
|
1767
|
+
const what = outgoing ? 'read' : 'msg';
|
|
1768
|
+
this._updateMyReadRecv(what, data.seq, data.ts);
|
|
1769
|
+
|
|
1770
|
+
if (!outgoing && data.from) {
|
|
1771
|
+
// Mark messages as read by the sender.
|
|
1772
|
+
this._routeInfo({
|
|
1773
|
+
what: 'read',
|
|
1774
|
+
from: data.from,
|
|
1775
|
+
seq: data.seq,
|
|
1776
|
+
_noForwarding: true
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Notify 'me' listeners of the change.
|
|
1781
|
+
this._tinode.getMeTopic()._refreshContact(what, this);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Process metadata message
|
|
1785
|
+
_routeMeta(meta) {
|
|
1786
|
+
if (meta.desc) {
|
|
1787
|
+
this._processMetaDesc(meta.desc);
|
|
1788
|
+
}
|
|
1789
|
+
if (meta.sub && meta.sub.length > 0) {
|
|
1790
|
+
this._processMetaSubs(meta.sub);
|
|
1791
|
+
}
|
|
1792
|
+
if (meta.del) {
|
|
1793
|
+
this._processDelMessages(meta.del.clear, meta.del.delseq);
|
|
1794
|
+
}
|
|
1795
|
+
if (meta.tags) {
|
|
1796
|
+
this._processMetaTags(meta.tags);
|
|
1797
|
+
}
|
|
1798
|
+
if (meta.cred) {
|
|
1799
|
+
this._processMetaCreds(meta.cred);
|
|
1800
|
+
}
|
|
1801
|
+
if (meta.aux) {
|
|
1802
|
+
this._processMetaAux(meta.aux);
|
|
1803
|
+
}
|
|
1804
|
+
if (this.onMeta) {
|
|
1805
|
+
this.onMeta(meta);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
// Process presence change message
|
|
1809
|
+
_routePres(pres) {
|
|
1810
|
+
let user, uid;
|
|
1811
|
+
switch (pres.what) {
|
|
1812
|
+
case 'del':
|
|
1813
|
+
// Delete cached messages.
|
|
1814
|
+
this._processDelMessages(pres.clear, pres.delseq);
|
|
1815
|
+
break;
|
|
1816
|
+
case 'on':
|
|
1817
|
+
case 'off':
|
|
1818
|
+
// Update online status of a subscription.
|
|
1819
|
+
user = this._users[pres.src];
|
|
1820
|
+
if (user) {
|
|
1821
|
+
user.online = pres.what == 'on';
|
|
1822
|
+
} else {
|
|
1823
|
+
this._tinode.logger("WARNING: Presence update for an unknown user", this.name, pres.src);
|
|
1824
|
+
}
|
|
1825
|
+
break;
|
|
1826
|
+
case 'term':
|
|
1827
|
+
// Attachment to topic is terminated probably due to cluster rehashing.
|
|
1828
|
+
this._resetSub();
|
|
1829
|
+
break;
|
|
1830
|
+
case 'upd':
|
|
1831
|
+
// A topic subscriber has updated his description.
|
|
1832
|
+
// Issue {get sub} only if the current user has no p2p topics with the updated user (p2p name is not in cache).
|
|
1833
|
+
// Otherwise 'me' will issue a {get desc} request.
|
|
1834
|
+
if (pres.src && !this._tinode.isTopicCached(pres.src)) {
|
|
1835
|
+
this.getMeta(this.startMetaQuery().withOneSub(undefined, pres.src).build());
|
|
1836
|
+
}
|
|
1837
|
+
break;
|
|
1838
|
+
case 'aux':
|
|
1839
|
+
// Auxiliary data updated.
|
|
1840
|
+
this.getMeta(this.startMetaQuery().withAux().build());
|
|
1841
|
+
break;
|
|
1842
|
+
case 'acs':
|
|
1843
|
+
uid = pres.src || this._tinode.getCurrentUserID();
|
|
1844
|
+
user = this._users[uid];
|
|
1845
|
+
if (!user) {
|
|
1846
|
+
// Update for an unknown user: notification of a new subscription.
|
|
1847
|
+
const acs = new AccessMode().updateAll(pres.dacs);
|
|
1848
|
+
if (acs && acs.mode != AccessMode._NONE) {
|
|
1849
|
+
user = this._cacheGetUser(uid);
|
|
1850
|
+
if (!user) {
|
|
1851
|
+
user = {
|
|
1852
|
+
user: uid,
|
|
1853
|
+
acs: acs
|
|
1854
|
+
};
|
|
1855
|
+
this.getMeta(this.startMetaQuery().withOneSub(undefined, uid).build());
|
|
1856
|
+
} else {
|
|
1857
|
+
user.acs = acs;
|
|
1858
|
+
}
|
|
1859
|
+
user.updated = new Date();
|
|
1860
|
+
this._processMetaSubs([user]);
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
// Known user
|
|
1864
|
+
user.acs.updateAll(pres.dacs);
|
|
1865
|
+
// Update user's access mode.
|
|
1866
|
+
this._processMetaSubs([{
|
|
1867
|
+
user: uid,
|
|
1868
|
+
updated: new Date(),
|
|
1869
|
+
acs: user.acs
|
|
1870
|
+
}]);
|
|
1871
|
+
}
|
|
1872
|
+
break;
|
|
1873
|
+
default:
|
|
1874
|
+
this._tinode.logger("INFO: Ignored presence update", pres.what);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
if (this.onPres) {
|
|
1878
|
+
this.onPres(pres);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
// Process {info} message
|
|
1882
|
+
_routeInfo(info) {
|
|
1883
|
+
switch (info.what) {
|
|
1884
|
+
case 'recv':
|
|
1885
|
+
case 'read':
|
|
1886
|
+
const user = this._users[info.from];
|
|
1887
|
+
if (user) {
|
|
1888
|
+
user[info.what] = info.seq;
|
|
1889
|
+
if (user.recv < user.read) {
|
|
1890
|
+
user.recv = user.read;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const msg = this.latestMessage();
|
|
1894
|
+
if (msg) {
|
|
1895
|
+
this.msgStatus(msg, true);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// If this is an update from the current user, update the cache with the new count.
|
|
1899
|
+
if (this._tinode.isMe(info.from) && !info._noForwarding) {
|
|
1900
|
+
this._updateMyReadRecv(info.what, info.seq);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Notify 'me' listener of the status change.
|
|
1904
|
+
this._tinode.getMeTopic()._refreshContact(info.what, this);
|
|
1905
|
+
break;
|
|
1906
|
+
case 'kp':
|
|
1907
|
+
case 'kpa':
|
|
1908
|
+
case 'kpv':
|
|
1909
|
+
// Typing or audio/video recording notification. Do nothing.
|
|
1910
|
+
break;
|
|
1911
|
+
case 'call':
|
|
1912
|
+
// Do nothing here.
|
|
1913
|
+
break;
|
|
1914
|
+
default:
|
|
1915
|
+
this._tinode.logger("INFO: Ignored info update", info.what);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (this.onInfo) {
|
|
1919
|
+
this.onInfo(info);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
// Called by Tinode when meta.desc packet is received.
|
|
1923
|
+
// Called by 'me' topic on contact update (desc._noForwarding is true).
|
|
1924
|
+
_processMetaDesc(desc) {
|
|
1925
|
+
if (this.isP2PType()) {
|
|
1926
|
+
// Synthetic desc may include defacs for p2p topics which is useless.
|
|
1927
|
+
// Remove it.
|
|
1928
|
+
delete desc.defacs;
|
|
1929
|
+
|
|
1930
|
+
// Update to p2p desc is the same as user update. Update cached user.
|
|
1931
|
+
this._tinode._db.updUser(this.name, desc.public);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// Copy parameters from desc object to this topic.
|
|
1935
|
+
mergeObj(this, desc);
|
|
1936
|
+
// Update persistent cache.
|
|
1937
|
+
this._tinode._db.updTopic(this);
|
|
1938
|
+
|
|
1939
|
+
// Notify 'me' listener, if available:
|
|
1940
|
+
if (this.name !== Const.TOPIC_ME && !desc._noForwarding) {
|
|
1941
|
+
const me = this._tinode.getMeTopic();
|
|
1942
|
+
if (me.onMetaSub) {
|
|
1943
|
+
me.onMetaSub(this);
|
|
1944
|
+
}
|
|
1945
|
+
if (me.onSubsUpdated) {
|
|
1946
|
+
me.onSubsUpdated([this.name], 1);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
if (this.onMetaDesc) {
|
|
1951
|
+
this.onMetaDesc(this);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
// Called by Tinode when meta.sub is recived, in response to received
|
|
1955
|
+
// {ctrl} after setMeta-sub, or as a handler for {pres what=sub}.
|
|
1956
|
+
_processMetaSubs(subs) {
|
|
1957
|
+
for (let idx in subs) {
|
|
1958
|
+
const sub = subs[idx];
|
|
1959
|
+
|
|
1960
|
+
// Fill defaults.
|
|
1961
|
+
sub.online = !!sub.online;
|
|
1962
|
+
// Update timestamp of the most recent subscription update.
|
|
1963
|
+
this._lastSubsUpdate = new Date(Math.max(this._lastSubsUpdate, sub.updated));
|
|
1964
|
+
|
|
1965
|
+
let user = null;
|
|
1966
|
+
if (!sub.deleted) {
|
|
1967
|
+
// If this is a change to user's own permissions, update them in topic too.
|
|
1968
|
+
// Desc will update 'me' topic.
|
|
1969
|
+
if (this._tinode.isMe(sub.user) && sub.acs) {
|
|
1970
|
+
this._processMetaDesc({
|
|
1971
|
+
updated: sub.updated,
|
|
1972
|
+
touched: sub.touched,
|
|
1973
|
+
acs: sub.acs
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
if (!this._users[sub.user]) {
|
|
1977
|
+
// New subscription.
|
|
1978
|
+
this.subcnt++;
|
|
1979
|
+
}
|
|
1980
|
+
user = this._updateCachedUser(sub.user, sub);
|
|
1981
|
+
} else {
|
|
1982
|
+
// Subscription is deleted, remove it from topic (but leave in Users cache)
|
|
1983
|
+
delete this._users[sub.user];
|
|
1984
|
+
user = sub;
|
|
1985
|
+
this.subcnt--;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (this.onMetaSub) {
|
|
1989
|
+
this.onMetaSub(user);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
if (this.onSubsUpdated) {
|
|
1994
|
+
this.onSubsUpdated(Object.keys(this._users));
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
// Called by Tinode when meta.tags is recived.
|
|
1998
|
+
_processMetaTags(tags) {
|
|
1999
|
+
if (tags == Const.DEL_CHAR || (tags.length == 1 && tags[0] == Const.DEL_CHAR)) {
|
|
2000
|
+
tags = [];
|
|
2001
|
+
}
|
|
2002
|
+
this._tags = tags;
|
|
2003
|
+
this._tinode._db.updTopic(this);
|
|
2004
|
+
if (this.onTagsUpdated) {
|
|
2005
|
+
this.onTagsUpdated(tags);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
// Do nothing for topics other than 'me'
|
|
2009
|
+
_processMetaCreds(creds) {}
|
|
2010
|
+
|
|
2011
|
+
// Called by Tinode when meta.aux is recived.
|
|
2012
|
+
_processMetaAux(aux) {
|
|
2013
|
+
aux = (!aux || aux == Const.DEL_CHAR) ? {} : aux;
|
|
2014
|
+
this._aux = mergeObj(this._aux, aux);
|
|
2015
|
+
this._tinode._db.updTopic(this);
|
|
2016
|
+
if (this.onAuxUpdated) {
|
|
2017
|
+
this.onAuxUpdated(this._aux);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// Delete cached messages and update cached transaction IDs
|
|
2022
|
+
_processDelMessages(clear, delseq) {
|
|
2023
|
+
this._maxDel = Math.max(clear, this._maxDel);
|
|
2024
|
+
this.clear = Math.max(clear, this.clear);
|
|
2025
|
+
let count = 0;
|
|
2026
|
+
if (Array.isArray(delseq)) {
|
|
2027
|
+
delseq.forEach(rec => {
|
|
2028
|
+
if (!rec.hi) {
|
|
2029
|
+
count++;
|
|
2030
|
+
this.flushMessage(rec.low);
|
|
2031
|
+
} else {
|
|
2032
|
+
count += rec.hi - rec.low;
|
|
2033
|
+
this.flushMessageRange(rec.low, rec.hi);
|
|
2034
|
+
}
|
|
2035
|
+
this._messages.put({
|
|
2036
|
+
seq: rec.low,
|
|
2037
|
+
low: rec.low,
|
|
2038
|
+
hi: rec.hi,
|
|
2039
|
+
_deleted: true
|
|
2040
|
+
});
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
this._tinode._db.addDelLog(this.name, clear, delseq);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (count > 0) {
|
|
2047
|
+
if (this.onData) {
|
|
2048
|
+
this.onData();
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
// Topic is informed that the entire response to {get what=data} has been received.
|
|
2053
|
+
_allMessagesReceived(count) {
|
|
2054
|
+
|
|
2055
|
+
if (this.onAllMessagesReceived) {
|
|
2056
|
+
this.onAllMessagesReceived(count);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
// Reset subscribed state
|
|
2060
|
+
_resetSub() {
|
|
2061
|
+
this._attached = false;
|
|
2062
|
+
}
|
|
2063
|
+
// This topic is either deleted or unsubscribed from.
|
|
2064
|
+
_gone() {
|
|
2065
|
+
this._messages.reset();
|
|
2066
|
+
this._tinode._db.remMessages(this.name);
|
|
2067
|
+
this._users = {};
|
|
2068
|
+
this.acs = new AccessMode(null);
|
|
2069
|
+
this.private = null;
|
|
2070
|
+
this.public = null;
|
|
2071
|
+
this.trusted = null;
|
|
2072
|
+
this._maxSeq = 0;
|
|
2073
|
+
this._minSeq = 0;
|
|
2074
|
+
this._attached = false;
|
|
2075
|
+
|
|
2076
|
+
const me = this._tinode.getMeTopic();
|
|
2077
|
+
if (me) {
|
|
2078
|
+
me._routePres({
|
|
2079
|
+
_noForwarding: true,
|
|
2080
|
+
what: 'gone',
|
|
2081
|
+
topic: Const.TOPIC_ME,
|
|
2082
|
+
src: this.name
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
if (this.onDeleteTopic) {
|
|
2086
|
+
this.onDeleteTopic();
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
// Update global user cache and local subscribers cache.
|
|
2090
|
+
// Don't call this method for non-subscribers.
|
|
2091
|
+
_updateCachedUser(uid, obj) {
|
|
2092
|
+
// Fetch user object from the global cache.
|
|
2093
|
+
// This is a clone of the stored object
|
|
2094
|
+
let cached = this._cacheGetUser(uid);
|
|
2095
|
+
cached = mergeObj(cached || {}, obj);
|
|
2096
|
+
// Save to global cache
|
|
2097
|
+
this._cachePutUser(uid, cached);
|
|
2098
|
+
// Save to the list of topic subsribers.
|
|
2099
|
+
return mergeToCache(this._users, uid, cached);
|
|
2100
|
+
}
|
|
2101
|
+
// Get local seqId for a queued message.
|
|
2102
|
+
_getQueuedSeqId() {
|
|
2103
|
+
return this._queuedSeqId++;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// Load most recent messages from persistent cache.
|
|
2107
|
+
_loadMessages(db, query) {
|
|
2108
|
+
console.log('[Topic] _loadMessages CALLED:', this.name, 'query:', JSON.stringify(query), 'db:', !!db);
|
|
2109
|
+
query = query || {};
|
|
2110
|
+
query.limit = query.limit || Const.DEFAULT_MESSAGES_PAGE;
|
|
2111
|
+
|
|
2112
|
+
// Count of message loaded from DB.
|
|
2113
|
+
let count = 0;
|
|
2114
|
+
console.log('[Topic] _loadMessages: calling db.readMessages');
|
|
2115
|
+
return db.readMessages(this.name, query)
|
|
2116
|
+
.then(msgs => {
|
|
2117
|
+
console.log('[Topic] _loadMessages: db.readMessages returned', msgs ? msgs.length : 0, 'messages');
|
|
2118
|
+
msgs.forEach(data => {
|
|
2119
|
+
if (data.seq > this._maxSeq) {
|
|
2120
|
+
this._maxSeq = data.seq;
|
|
2121
|
+
}
|
|
2122
|
+
if (data.seq < this._minSeq || this._minSeq == 0) {
|
|
2123
|
+
this._minSeq = data.seq;
|
|
2124
|
+
}
|
|
2125
|
+
this._messages.put(data);
|
|
2126
|
+
this._maybeUpdateMessageVersionsCache(data);
|
|
2127
|
+
});
|
|
2128
|
+
count = msgs.length;
|
|
2129
|
+
console.log('[Topic] _loadMessages: processed', count, 'messages, _maxSeq:', this._maxSeq, '_minSeq:', this._minSeq);
|
|
2130
|
+
})
|
|
2131
|
+
.then(_ => db.readDelLog(this.name, query))
|
|
2132
|
+
.then(dellog => {
|
|
2133
|
+
return dellog.forEach(rec => {
|
|
2134
|
+
this._messages.put({
|
|
2135
|
+
seq: rec.low,
|
|
2136
|
+
low: rec.low,
|
|
2137
|
+
hi: rec.hi,
|
|
2138
|
+
_deleted: true
|
|
2139
|
+
});
|
|
2140
|
+
});
|
|
2141
|
+
})
|
|
2142
|
+
.then(_ => {
|
|
2143
|
+
// DEBUG
|
|
2144
|
+
return count;
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// Push or {pres}: message received.
|
|
2149
|
+
_updateReceived(seq, act) {
|
|
2150
|
+
this.touched = new Date();
|
|
2151
|
+
this.seq = seq | 0;
|
|
2152
|
+
// Check if message is sent by the current user. If so it's been read already.
|
|
2153
|
+
if (!act || this._tinode.isMe(act)) {
|
|
2154
|
+
this.read = this.read ? Math.max(this.read, this.seq) : this.seq;
|
|
2155
|
+
this.recv = this.recv ? Math.max(this.read, this.recv) : this.read;
|
|
2156
|
+
}
|
|
2157
|
+
this.unread = this.seq - (this.read | 0);
|
|
2158
|
+
this._tinode._db.updTopic(this);
|
|
2159
|
+
}
|
|
2160
|
+
}
|