@onyx-p/imlib-web 3.0.4 → 3.0.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.
package/README.md CHANGED
@@ -106,7 +106,8 @@ already committed batches are safe to repeat.
106
106
  ## 消息搜索
107
107
 
108
108
  单会话搜索接口保持不变。全局搜索使用 Signal 风格的 FTS5 前缀匹配,
109
- 按消息时间倒序返回,默认最多 500 条:
109
+ 按消息时间倒序返回,默认最多 500 条。文本消息按正文搜索,
110
+ 文件消息按文件名搜索:
110
111
 
111
112
  ```js
112
113
  const result = await ACIMLib.searchMessages('项目进度')
package/database/main.cjs CHANGED
@@ -28,6 +28,14 @@ class DatabaseKeyStore {
28
28
  this.writeKeys(keys);
29
29
  return key;
30
30
  }
31
+ remove(accountId) {
32
+ const keys = this.readKeys();
33
+ if (!(accountId in keys)) {
34
+ return;
35
+ }
36
+ delete keys[accountId];
37
+ this.writeKeys(keys);
38
+ }
31
39
  readKeys() {
32
40
  try {
33
41
  const parsed = JSON.parse(node_fs.readFileSync(this.filePath, 'utf8'));
@@ -69,6 +77,7 @@ const READ_METHOD_NAMES = [
69
77
  'getPinnedMessages',
70
78
  'searchMessages',
71
79
  'searchTextMessages',
80
+ 'getLegacyMediaReferences',
72
81
  'getLegacyMigrationState'
73
82
  ];
74
83
  const WRITE_METHOD_NAMES = [
@@ -78,6 +87,7 @@ const WRITE_METHOD_NAMES = [
78
87
  'removeMessagesByUId',
79
88
  'updateMessageReceiptStatus',
80
89
  'setMessagePinned',
90
+ 'setMessageReactions',
81
91
  'unpinAllMessages',
82
92
  'clearBurnAfterReadingExpiredMessages',
83
93
  'upsertMessage',
@@ -120,6 +130,19 @@ function parseRequest(value) {
120
130
  if (value.type === 'close') {
121
131
  return { type: 'close' };
122
132
  }
133
+ if (value.type === 'delete') {
134
+ if (!isRecord(value.options) ||
135
+ typeof value.options.appKey !== 'string' ||
136
+ typeof value.options.userId !== 'string' ||
137
+ !value.options.appKey ||
138
+ !value.options.userId) {
139
+ throw new Error('Invalid database delete options');
140
+ }
141
+ return {
142
+ type: 'delete',
143
+ options: { appKey: value.options.appKey, userId: value.options.userId }
144
+ };
145
+ }
123
146
  if (value.type === 'open') {
124
147
  if (!isRecord(value.options) ||
125
148
  typeof value.options.appKey !== 'string' ||
@@ -158,6 +181,9 @@ function registerDatabaseIpc({ ipcMain, service, isTrustedEvent = isTopLevelSend
158
181
  else if (request.type === 'close') {
159
182
  result = await service.close();
160
183
  }
184
+ else if (request.type === 'delete') {
185
+ result = await service.deleteAccount(request.options);
186
+ }
161
187
  else if (request.type === 'read') {
162
188
  result = await service.read(request.method, request.args);
163
189
  }
@@ -250,6 +276,18 @@ class MainDatabaseService {
250
276
  }
251
277
  }));
252
278
  }
279
+ async deleteAccount(options) {
280
+ await this.close();
281
+ const accountId = node_crypto.createHash('sha256')
282
+ .update(`${options.appKey}:${options.userId}`)
283
+ .digest('hex');
284
+ const sqlDirectory = node_path.join(this.options.userDataPath, 'sql');
285
+ const dbPath = node_path.join(sqlDirectory, `${accountId}.sqlite`);
286
+ for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
287
+ node_fs.rmSync(path, { force: true });
288
+ }
289
+ this.options.keyStore.remove(accountId);
290
+ }
253
291
  assertOpen() {
254
292
  if (!this.accountId || this.pool.length === 0) {
255
293
  throw new Error('Database service is not open');
@@ -16,6 +16,7 @@ const READ_METHOD_NAMES = [
16
16
  'getPinnedMessages',
17
17
  'searchMessages',
18
18
  'searchTextMessages',
19
+ 'getLegacyMediaReferences',
19
20
  'getLegacyMigrationState'
20
21
  ];
21
22
  const WRITE_METHOD_NAMES = [
@@ -25,6 +26,7 @@ const WRITE_METHOD_NAMES = [
25
26
  'removeMessagesByUId',
26
27
  'updateMessageReceiptStatus',
27
28
  'setMessagePinned',
29
+ 'setMessageReactions',
28
30
  'unpinAllMessages',
29
31
  'clearBurnAfterReadingExpiredMessages',
30
32
  'upsertMessage',
@@ -19,6 +19,7 @@ const READ_METHOD_NAMES = [
19
19
  'getPinnedMessages',
20
20
  'searchMessages',
21
21
  'searchTextMessages',
22
+ 'getLegacyMediaReferences',
22
23
  'getLegacyMigrationState'
23
24
  ];
24
25
  const WRITE_METHOD_NAMES = [
@@ -28,6 +29,7 @@ const WRITE_METHOD_NAMES = [
28
29
  'removeMessagesByUId',
29
30
  'updateMessageReceiptStatus',
30
31
  'setMessagePinned',
32
+ 'setMessageReactions',
31
33
  'unpinAllMessages',
32
34
  'clearBurnAfterReadingExpiredMessages',
33
35
  'upsertMessage',
@@ -52,6 +54,24 @@ function serializeDatabaseError(error) {
52
54
  };
53
55
  }
54
56
 
57
+ const MessageTypes = {
58
+ TEXT: 0x00000000,
59
+ IMAGE: 0x30001000,
60
+ GIF: 0x3000100B,
61
+ FILE: 0x3000101A,
62
+ AUDIO: 0x30001011,
63
+ VIDEO: 0x30001006,
64
+ RECALL: 0x6001001e,
65
+ STORED_RECALL: 0x6001001d,
66
+ LOCATION: 0x30001005,
67
+ CHAT_RECORD: 0x30001019,
68
+ CONTACT: 0x10001002,
69
+ GROUP_INVITATION: 0x30001003,
70
+ REDPACKET: 0x10001040,
71
+ TRANSFER: 0x10001030,
72
+ LINK: 0x10001004
73
+ };
74
+
55
75
  const SCHEMA_MIGRATIONS = [
56
76
  {
57
77
  version: 1,
@@ -157,6 +177,20 @@ const SCHEMA_MIGRATIONS = [
157
177
  ON messages (dialogId, pinned, sentTime DESC, messageId DESC);
158
178
  `);
159
179
  }
180
+ },
181
+ {
182
+ version: 7,
183
+ up(database) {
184
+ database
185
+ .prepare(`UPDATE messages
186
+ SET searchText = CASE
187
+ WHEN json_type(json, '$.content.title') = 'text'
188
+ THEN json_extract(json, '$.content.title')
189
+ ELSE ''
190
+ END
191
+ WHERE messageType = $fileMessageType;`)
192
+ .run({ fileMessageType: MessageTypes.FILE });
193
+ }
160
194
  }
161
195
  ];
162
196
  SCHEMA_MIGRATIONS[SCHEMA_MIGRATIONS.length - 1]?.version ?? 0;
@@ -187,6 +221,167 @@ function runMigrations(database, migrations = SCHEMA_MIGRATIONS) {
187
221
  }
188
222
  }
189
223
 
224
+ var ConnectionStatus;
225
+ (function (ConnectionStatus) {
226
+ ConnectionStatus[ConnectionStatus["CONNECTED"] = 0] = "CONNECTED";
227
+ ConnectionStatus[ConnectionStatus["CONNECTING"] = 1] = "CONNECTING";
228
+ ConnectionStatus[ConnectionStatus["DISCONNECTED"] = 2] = "DISCONNECTED";
229
+ ConnectionStatus[ConnectionStatus["NETWORK_UNAVAILABLE"] = 3] = "NETWORK_UNAVAILABLE";
230
+ ConnectionStatus[ConnectionStatus["CONNECTION_CLOSED"] = 4] = "CONNECTION_CLOSED";
231
+ ConnectionStatus[ConnectionStatus["KICKED_OFFLINE_BY_OTHER_CLIENT"] = 6] = "KICKED_OFFLINE_BY_OTHER_CLIENT";
232
+ ConnectionStatus[ConnectionStatus["WEBSOCKET_UNAVAILABLE"] = 7] = "WEBSOCKET_UNAVAILABLE";
233
+ ConnectionStatus[ConnectionStatus["WEBSOCKET_ERROR"] = 8] = "WEBSOCKET_ERROR";
234
+ ConnectionStatus[ConnectionStatus["BLOCKED"] = 9] = "BLOCKED";
235
+ ConnectionStatus[ConnectionStatus["DISCONNECT_BY_SERVER"] = 13] = "DISCONNECT_BY_SERVER";
236
+ })(ConnectionStatus || (ConnectionStatus = {}));
237
+ var ErrorCode;
238
+ (function (ErrorCode) {
239
+ ErrorCode[ErrorCode["TIMEOUT"] = -1] = "TIMEOUT";
240
+ ErrorCode[ErrorCode["UNKNOWN"] = -2] = "UNKNOWN";
241
+ ErrorCode[ErrorCode["PARAMETER_ERROR"] = -3] = "PARAMETER_ERROR";
242
+ ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
243
+ ErrorCode[ErrorCode["NETWORK_ERROR"] = 7] = "NETWORK_ERROR";
244
+ ErrorCode[ErrorCode["MSG_ENCRYPT_ERROR"] = 6001] = "MSG_ENCRYPT_ERROR";
245
+ ErrorCode[ErrorCode["RC_DISCUSSION_GROUP_ID_INVALID"] = 20407] = "RC_DISCUSSION_GROUP_ID_INVALID";
246
+ ErrorCode[ErrorCode["SEND_FREQUENCY_TOO_FAST"] = 20604] = "SEND_FREQUENCY_TOO_FAST";
247
+ ErrorCode[ErrorCode["FORBIDDEN_IN_GROUP"] = 22408] = "FORBIDDEN_IN_GROUP";
248
+ ErrorCode[ErrorCode["RECALL_MESSAGE"] = 25101] = "RECALL_MESSAGE";
249
+ ErrorCode[ErrorCode["NOT_IN_GROUP"] = 22406] = "NOT_IN_GROUP";
250
+ ErrorCode[ErrorCode["SENSITIVE_SHIELD"] = 21501] = "SENSITIVE_SHIELD";
251
+ ErrorCode[ErrorCode["GET_USERINFO_ERROR"] = 23407] = "GET_USERINFO_ERROR";
252
+ ErrorCode[ErrorCode["REJECTED_BY_BLACKLIST"] = 405] = "REJECTED_BY_BLACKLIST";
253
+ ErrorCode[ErrorCode["RC_NET_CHANNEL_INVALID"] = 30001] = "RC_NET_CHANNEL_INVALID";
254
+ ErrorCode[ErrorCode["RC_NET_UNAVAILABLE"] = 30002] = "RC_NET_UNAVAILABLE";
255
+ ErrorCode[ErrorCode["RC_MSG_RESP_TIMEOUT"] = 30003] = "RC_MSG_RESP_TIMEOUT";
256
+ ErrorCode[ErrorCode["RC_SOCKET_NOT_CREATED"] = 30010] = "RC_SOCKET_NOT_CREATED";
257
+ ErrorCode[ErrorCode["RC_SOCKET_DISCONNECTED"] = 30011] = "RC_SOCKET_DISCONNECTED";
258
+ ErrorCode[ErrorCode["RC_PONG_RECV_FAIL"] = 30013] = "RC_PONG_RECV_FAIL";
259
+ ErrorCode[ErrorCode["RC_MSG_SEND_FAIL"] = 30014] = "RC_MSG_SEND_FAIL";
260
+ ErrorCode[ErrorCode["RC_MSG_CONTENT_EXCEED_LIMIT"] = 30016] = "RC_MSG_CONTENT_EXCEED_LIMIT";
261
+ ErrorCode[ErrorCode["RC_CONN_ACK_TIMEOUT"] = 31000] = "RC_CONN_ACK_TIMEOUT";
262
+ ErrorCode[ErrorCode["RC_CONN_ID_REJECT"] = 31002] = "RC_CONN_ID_REJECT";
263
+ ErrorCode[ErrorCode["RC_CONN_SERVER_UNAVAILABLE"] = 31003] = "RC_CONN_SERVER_UNAVAILABLE";
264
+ ErrorCode[ErrorCode["RC_CONN_USER_OR_PASSWD_ERROR"] = 31004] = "RC_CONN_USER_OR_PASSWD_ERROR";
265
+ ErrorCode[ErrorCode["RC_CONN_NOT_AUTHRORIZED"] = 31005] = "RC_CONN_NOT_AUTHRORIZED";
266
+ ErrorCode[ErrorCode["RC_DISCONN_KICK"] = 31010] = "RC_DISCONN_KICK";
267
+ ErrorCode[ErrorCode["RC_DISCONN_SAME_CLIENT_ON_LINE"] = 31023] = "RC_DISCONN_SAME_CLIENT_ON_LINE";
268
+ ErrorCode[ErrorCode["BIZ_ERROR_CLIENT_NOT_INIT"] = 33001] = "BIZ_ERROR_CLIENT_NOT_INIT";
269
+ ErrorCode[ErrorCode["BIZ_ERROR_INVALID_PARAMETER"] = 33003] = "BIZ_ERROR_INVALID_PARAMETER";
270
+ ErrorCode[ErrorCode["BIZ_ERROR_RECONNECT_SUCCESS"] = 33005] = "BIZ_ERROR_RECONNECT_SUCCESS";
271
+ ErrorCode[ErrorCode["BIZ_ERROR_CONNECTING"] = 33006] = "BIZ_ERROR_CONNECTING";
272
+ ErrorCode[ErrorCode["MSG_INSERT_ERROR"] = 33008] = "MSG_INSERT_ERROR";
273
+ ErrorCode[ErrorCode["MSG_DEL_ERROR"] = 33009] = "MSG_DEL_ERROR";
274
+ ErrorCode[ErrorCode["CONVER_REMOVE_ERROR"] = 34001] = "CONVER_REMOVE_ERROR";
275
+ ErrorCode[ErrorCode["CONVER_GETLIST_ERROR"] = 34002] = "CONVER_GETLIST_ERROR";
276
+ ErrorCode[ErrorCode["CONVER_SETOP_ERROR"] = 34003] = "CONVER_SETOP_ERROR";
277
+ ErrorCode[ErrorCode["CONVER_TOTAL_UNREAD_ERROR"] = 34004] = "CONVER_TOTAL_UNREAD_ERROR";
278
+ ErrorCode[ErrorCode["CONVER_TYPE_UNREAD_ERROR"] = 34005] = "CONVER_TYPE_UNREAD_ERROR";
279
+ ErrorCode[ErrorCode["CONVER_ID_TYPE_UNREAD_ERROR"] = 34006] = "CONVER_ID_TYPE_UNREAD_ERROR";
280
+ ErrorCode[ErrorCode["CONVER_OUT_LIMIT_ERROR"] = 34013] = "CONVER_OUT_LIMIT_ERROR";
281
+ ErrorCode[ErrorCode["MEDIA_EXCEPTION"] = 34018] = "MEDIA_EXCEPTION";
282
+ ErrorCode[ErrorCode["CONVER_GET_ERROR"] = 35021] = "CONVER_GET_ERROR";
283
+ ErrorCode[ErrorCode["GROUP_SYNC_ERROR"] = 35001] = "GROUP_SYNC_ERROR";
284
+ ErrorCode[ErrorCode["CAN_NOT_RECONNECT"] = 35007] = "CAN_NOT_RECONNECT";
285
+ ErrorCode[ErrorCode["HAS_OHTER_SAME_CLIENT_ON_LINE"] = 35010] = "HAS_OHTER_SAME_CLIENT_ON_LINE";
286
+ ErrorCode[ErrorCode["METHOD_NOT_AVAILABLE"] = 35011] = "METHOD_NOT_AVAILABLE";
287
+ ErrorCode[ErrorCode["METHOD_NOT_SUPPORT"] = 35012] = "METHOD_NOT_SUPPORT";
288
+ ErrorCode[ErrorCode["MSG_LIMIT_ERROR"] = 35013] = "MSG_LIMIT_ERROR";
289
+ ErrorCode[ErrorCode["METHOD_ONLY_SUPPORT_ULTRA_GROUP"] = 35014] = "METHOD_ONLY_SUPPORT_ULTRA_GROUP";
290
+ ErrorCode[ErrorCode["UPLOAD_FILE_FAILED"] = 35020] = "UPLOAD_FILE_FAILED";
291
+ ErrorCode[ErrorCode["DRAF_GET_ERROR"] = 38001] = "DRAF_GET_ERROR";
292
+ ErrorCode[ErrorCode["DRAF_SAVE_ERROR"] = 38002] = "DRAF_SAVE_ERROR";
293
+ ErrorCode[ErrorCode["DRAF_REMOVE_ERROR"] = 38003] = "DRAF_REMOVE_ERROR";
294
+ ErrorCode[ErrorCode["NOT_SUPPORT"] = 39002] = "NOT_SUPPORT";
295
+ })(ErrorCode || (ErrorCode = {}));
296
+ var ConversationType;
297
+ (function (ConversationType) {
298
+ ConversationType[ConversationType["PRIVATE"] = 1] = "PRIVATE";
299
+ ConversationType[ConversationType["GROUP"] = 3] = "GROUP";
300
+ })(ConversationType || (ConversationType = {}));
301
+ var MentionedType;
302
+ (function (MentionedType) {
303
+ MentionedType[MentionedType["ALL"] = 1] = "ALL";
304
+ MentionedType[MentionedType["SINGAL"] = 2] = "SINGAL";
305
+ })(MentionedType || (MentionedType = {}));
306
+ var NotificationStatus;
307
+ (function (NotificationStatus) {
308
+ NotificationStatus[NotificationStatus["OPEN"] = 1] = "OPEN";
309
+ NotificationStatus[NotificationStatus["CLOSE"] = 2] = "CLOSE";
310
+ })(NotificationStatus || (NotificationStatus = {}));
311
+ var MessageDirection;
312
+ (function (MessageDirection) {
313
+ MessageDirection[MessageDirection["SEND"] = 1] = "SEND";
314
+ MessageDirection[MessageDirection["RECEIVE"] = 2] = "RECEIVE";
315
+ })(MessageDirection || (MessageDirection = {}));
316
+ var SentStatus;
317
+ (function (SentStatus) {
318
+ SentStatus[SentStatus["SENDING"] = 10] = "SENDING";
319
+ SentStatus[SentStatus["FAILED"] = 20] = "FAILED";
320
+ SentStatus[SentStatus["SENT"] = 30] = "SENT";
321
+ })(SentStatus || (SentStatus = {}));
322
+ var ReceivedStatus;
323
+ (function (ReceivedStatus) {
324
+ ReceivedStatus[ReceivedStatus["IDLE"] = 0] = "IDLE";
325
+ ReceivedStatus[ReceivedStatus["RECEIVED"] = 1] = "RECEIVED";
326
+ ReceivedStatus[ReceivedStatus["READ"] = 2] = "READ";
327
+ ReceivedStatus[ReceivedStatus["LISTENED"] = 3] = "LISTENED";
328
+ })(ReceivedStatus || (ReceivedStatus = {}));
329
+ var NotificationLevel;
330
+ (function (NotificationLevel) {
331
+ NotificationLevel[NotificationLevel["ALL_MESSAGE"] = -1] = "ALL_MESSAGE";
332
+ NotificationLevel[NotificationLevel["NOT_SET"] = 0] = "NOT_SET";
333
+ NotificationLevel[NotificationLevel["AT_MESSAGE_NOTIFICATION"] = 1] = "AT_MESSAGE_NOTIFICATION";
334
+ NotificationLevel[NotificationLevel["AT_USER_NOTIFICATION"] = 2] = "AT_USER_NOTIFICATION";
335
+ NotificationLevel[NotificationLevel["AT_GROUP_ALL_USER_NOTIFICATION"] = 4] = "AT_GROUP_ALL_USER_NOTIFICATION";
336
+ NotificationLevel[NotificationLevel["NOT_MESSAGE_NOTIFICATION"] = 5] = "NOT_MESSAGE_NOTIFICATION";
337
+ })(NotificationLevel || (NotificationLevel = {}));
338
+
339
+ function extractLegacyMediaReferences(message) {
340
+ const { messageId, conversationType, targetId } = message;
341
+ if (!Number.isSafeInteger(messageId) ||
342
+ typeof conversationType !== 'number' ||
343
+ !Number.isSafeInteger(conversationType) ||
344
+ typeof targetId !== 'string' ||
345
+ !targetId) {
346
+ return [];
347
+ }
348
+ const result = [];
349
+ const content = message.content ?? {};
350
+ const add = (kind, value) => {
351
+ const objectKey = typeof value === 'string' ? value.trim() : '';
352
+ if (!objectKey) {
353
+ return;
354
+ }
355
+ result.push({
356
+ messageId,
357
+ conversationType,
358
+ targetId,
359
+ objectKey,
360
+ kind
361
+ });
362
+ };
363
+ switch (message.messageType) {
364
+ case MessageTypes.IMAGE:
365
+ add('image', content.originalObjectKey);
366
+ add('image-thumbnail', content.thumbnailObjectKey);
367
+ break;
368
+ case MessageTypes.GIF:
369
+ add('gif', content.originalObjectKey);
370
+ break;
371
+ case MessageTypes.VIDEO:
372
+ add('video', content.videoObjectKey);
373
+ add('video-thumbnail', content.thumbnailObjectKey);
374
+ break;
375
+ case MessageTypes.FILE:
376
+ add('file', content.fileKey);
377
+ break;
378
+ case MessageTypes.AUDIO:
379
+ add('audio', content.audioObjectKey);
380
+ break;
381
+ }
382
+ return result;
383
+ }
384
+
190
385
  const LEGACY_MIGRATION_KEY = 'legacy_indexeddb_migration';
191
386
  function parseMessage(row) {
192
387
  if (!row) {
@@ -196,8 +391,38 @@ function parseMessage(row) {
196
391
  message.pinned = message.pinned ?? false;
197
392
  return message;
198
393
  }
394
+ function mergeStoredReceiptStatus(incoming, existing) {
395
+ if (!existing) {
396
+ return incoming;
397
+ }
398
+ const existingStatus = existing.receivedStatus ?? -1;
399
+ const incomingStatus = incoming.receivedStatus ?? -1;
400
+ const merged = existingStatus <= incomingStatus
401
+ ? {
402
+ ...incoming,
403
+ messageId: existing.messageId
404
+ }
405
+ : {
406
+ ...incoming,
407
+ messageId: existing.messageId,
408
+ receivedStatus: existing.receivedStatus
409
+ };
410
+ if (incoming.reactions == null && existing.reactions != null) {
411
+ merged.reactions = existing.reactions;
412
+ }
413
+ if (incoming.burnAfterReadingStartedAt == null &&
414
+ existing.burnAfterReadingStartedAt != null) {
415
+ merged.burnAfterReadingStartedAt = existing.burnAfterReadingStartedAt;
416
+ }
417
+ return merged;
418
+ }
199
419
  function extractSearchText(message) {
200
420
  const content = message.content;
421
+ if (message.messageType === MessageTypes.FILE &&
422
+ content &&
423
+ typeof content.title === 'string') {
424
+ return content.title;
425
+ }
201
426
  if (typeof content === 'string') {
202
427
  return content;
203
428
  }
@@ -213,6 +438,32 @@ function normalizeInteger(value, name) {
213
438
  }
214
439
  return number;
215
440
  }
441
+ function isBurnAfterReadingMessageVisible(message, now) {
442
+ if (!message.burnAfterReadingFlag) {
443
+ return true;
444
+ }
445
+ const burnAfterReadingTime = message.burnAfterReadingTime ?? 0;
446
+ const sentTime = Number(message.sentTime);
447
+ if (message.messageDirection === MessageDirection.RECEIVE) {
448
+ if ((message.receivedStatus ?? ReceivedStatus.IDLE) < ReceivedStatus.READ) {
449
+ return true;
450
+ }
451
+ if (burnAfterReadingTime <= 0) {
452
+ return false;
453
+ }
454
+ const startedAt = message.burnAfterReadingStartedAt ?? sentTime;
455
+ return !Number.isFinite(startedAt) || startedAt + burnAfterReadingTime > now;
456
+ }
457
+ if (message.messageDirection === MessageDirection.SEND) {
458
+ if (message.sentStatus !== SentStatus.SENT) {
459
+ return true;
460
+ }
461
+ return (burnAfterReadingTime > 0 &&
462
+ (!Number.isFinite(sentTime) || sentTime + burnAfterReadingTime > now));
463
+ }
464
+ return (burnAfterReadingTime > 0 &&
465
+ (!Number.isFinite(sentTime) || sentTime + burnAfterReadingTime > now));
466
+ }
216
467
  class MessageStore {
217
468
  database;
218
469
  constructor(database) {
@@ -244,20 +495,24 @@ class MessageStore {
244
495
  if (message.isPersited === false) {
245
496
  continue;
246
497
  }
247
- const messageId = normalizeInteger(message.messageId, 'messageId');
248
- const sentTime = normalizeInteger(message.sentTime, 'sentTime');
498
+ const existing = message.messageUId
499
+ ? this.getMessageByUId(message.messageUId)
500
+ : this.getMessageById(normalizeInteger(message.messageId, 'messageId'));
501
+ const merged = mergeStoredReceiptStatus(message, existing);
502
+ const messageId = normalizeInteger(merged.messageId, 'messageId');
503
+ const sentTime = normalizeInteger(merged.sentTime, 'sentTime');
249
504
  upsert.run({
250
505
  messageId,
251
- messageUId: message.messageUId ?? null,
506
+ messageUId: merged.messageUId ?? null,
252
507
  dialogId,
253
508
  sentTime,
254
- receivedStatus: message.receivedStatus ?? null,
255
- burnAfterReadingFlag: message.burnAfterReadingFlag ? 1 : 0,
256
- burnAfterReadingTime: message.burnAfterReadingTime ?? null,
257
- messageType: message.messageType ?? null,
258
- pinned: message.pinned ? 1 : 0,
259
- json: JSON.stringify(message),
260
- searchText: extractSearchText(message)
509
+ receivedStatus: merged.receivedStatus ?? null,
510
+ burnAfterReadingFlag: merged.burnAfterReadingFlag ? 1 : 0,
511
+ burnAfterReadingTime: merged.burnAfterReadingTime ?? null,
512
+ messageType: merged.messageType ?? null,
513
+ pinned: merged.pinned ? 1 : 0,
514
+ json: JSON.stringify(merged),
515
+ searchText: extractSearchText(merged)
261
516
  });
262
517
  }
263
518
  if (isEnd !== undefined) {
@@ -270,33 +525,54 @@ class MessageStore {
270
525
  }
271
526
  getMessages(dialogId, timestamp = '0', count = 20, isForward = true) {
272
527
  const safeCount = Math.max(0, Math.floor(count));
528
+ const batchSize = Math.max(safeCount + 1, 50);
273
529
  const params = {
274
530
  dialogId,
275
- limit: safeCount + 1
531
+ limit: batchSize
276
532
  };
533
+ let offset = 0;
277
534
  let timeClause = '';
278
535
  if (timestamp !== '0') {
279
536
  params.timestamp = normalizeInteger(timestamp, 'timestamp');
280
- timeClause = isForward ? 'AND sentTime < $timestamp' : 'AND sentTime > $timestamp';
537
+ timeClause = isForward
538
+ ? 'AND sentTime < $timestamp'
539
+ : 'AND sentTime > $timestamp';
281
540
  }
282
541
  const direction = isForward ? 'DESC' : 'ASC';
283
- const rows = this.database
284
- .prepare(`SELECT json FROM messages
285
- WHERE dialogId = $dialogId ${timeClause}
286
- ORDER BY sentTime ${direction}, messageId ${direction}
287
- LIMIT $limit;`)
288
- .all(params);
289
- const hasAdditionalRow = rows.length > safeCount;
290
- const messages = rows
542
+ const select = this.database.prepare(`SELECT json FROM messages
543
+ WHERE dialogId = $dialogId ${timeClause}
544
+ ORDER BY sentTime ${direction}, messageId ${direction}
545
+ LIMIT $limit OFFSET $offset;`);
546
+ const visibleMessages = [];
547
+ const now = Date.now();
548
+ while (visibleMessages.length <= safeCount) {
549
+ const rows = select.all({ ...params, offset });
550
+ for (const row of rows) {
551
+ const message = parseMessage(row);
552
+ if (message && isBurnAfterReadingMessageVisible(message, now)) {
553
+ visibleMessages.push(message);
554
+ if (visibleMessages.length > safeCount) {
555
+ break;
556
+ }
557
+ }
558
+ }
559
+ if (rows.length < batchSize || visibleMessages.length > safeCount) {
560
+ break;
561
+ }
562
+ offset += rows.length;
563
+ }
564
+ const hasAdditionalMessage = visibleMessages.length > safeCount;
565
+ const messages = visibleMessages
291
566
  .slice(0, safeCount)
292
- .map(row => parseMessage(row))
293
567
  .sort((left, right) => {
294
568
  const byTime = Number(left.sentTime) - Number(right.sentTime);
295
569
  return byTime || left.messageId - right.messageId;
296
570
  });
297
571
  return {
298
572
  messages,
299
- hasMore: hasAdditionalRow || !this.getDialogLoadedState(dialogId)
573
+ hasMore: isForward
574
+ ? hasAdditionalMessage || !this.getDialogLoadedState(dialogId)
575
+ : hasAdditionalMessage
300
576
  };
301
577
  }
302
578
  getMessagesByType(dialogId, messageTypes, timestamp = '0', count = 50, isForward = true) {
@@ -431,7 +707,7 @@ class MessageStore {
431
707
  }
432
708
  })();
433
709
  }
434
- updateMessageReceiptStatus(messageUIds, receivedStatus) {
710
+ updateMessageReceiptStatus(messageUIds, receivedStatus, burnAfterReadingStartedAt) {
435
711
  const select = this.database.prepare('SELECT json, receivedStatus FROM messages WHERE messageUId = $messageUId;');
436
712
  const update = this.database.prepare(`
437
713
  UPDATE messages SET receivedStatus = $receivedStatus, json = $json
@@ -440,12 +716,31 @@ class MessageStore {
440
716
  this.database.transaction(() => {
441
717
  for (const messageUId of messageUIds) {
442
718
  const row = select.get({ messageUId });
443
- if (!row || (row.receivedStatus ?? -1) >= receivedStatus) {
719
+ if (!row) {
444
720
  continue;
445
721
  }
446
722
  const message = JSON.parse(row.json);
447
- message.receivedStatus = receivedStatus;
448
- update.run({ messageUId, receivedStatus, json: JSON.stringify(message) });
723
+ const shouldAdvanceStatus = (row.receivedStatus ?? -1) < receivedStatus;
724
+ const shouldStartBurn = burnAfterReadingStartedAt != null &&
725
+ receivedStatus === ReceivedStatus.READ &&
726
+ message.messageDirection === MessageDirection.RECEIVE &&
727
+ message.burnAfterReadingFlag === true &&
728
+ message.burnAfterReadingStartedAt == null;
729
+ if (!shouldAdvanceStatus && !shouldStartBurn) {
730
+ continue;
731
+ }
732
+ const nextReceivedStatus = shouldAdvanceStatus
733
+ ? receivedStatus
734
+ : row.receivedStatus ?? receivedStatus;
735
+ message.receivedStatus = nextReceivedStatus;
736
+ if (shouldStartBurn) {
737
+ message.burnAfterReadingStartedAt = burnAfterReadingStartedAt;
738
+ }
739
+ update.run({
740
+ messageUId,
741
+ receivedStatus: nextReceivedStatus,
742
+ json: JSON.stringify(message)
743
+ });
449
744
  }
450
745
  })();
451
746
  }
@@ -469,6 +764,31 @@ class MessageStore {
469
764
  json: JSON.stringify(message)
470
765
  });
471
766
  }
767
+ setMessageReactions(dialogId, messageUId, reactions) {
768
+ const row = this.database
769
+ .prepare(`SELECT json FROM messages
770
+ WHERE dialogId = $dialogId AND messageUId = $messageUId;`)
771
+ .get({ dialogId, messageUId });
772
+ const message = parseMessage(row);
773
+ if (!message) {
774
+ return false;
775
+ }
776
+ if (reactions.length) {
777
+ message.reactions = reactions;
778
+ }
779
+ else {
780
+ delete message.reactions;
781
+ }
782
+ this.database
783
+ .prepare(`UPDATE messages SET json = $json
784
+ WHERE dialogId = $dialogId AND messageUId = $messageUId;`)
785
+ .run({
786
+ dialogId,
787
+ messageUId,
788
+ json: JSON.stringify(message)
789
+ });
790
+ return true;
791
+ }
472
792
  unpinAllMessages(dialogId) {
473
793
  const rows = this.database
474
794
  .prepare(`SELECT messageUId, json FROM messages
@@ -493,20 +813,23 @@ class MessageStore {
493
813
  }
494
814
  clearBurnAfterReadingExpiredMessages(dialogId, now = Date.now()) {
495
815
  const rows = this.database
496
- .prepare(`SELECT messageUId FROM messages
497
- WHERE dialogId = $dialogId
498
- AND burnAfterReadingFlag = 1
499
- AND (burnAfterReadingTime IS NULL OR burnAfterReadingTime = 0
500
- OR sentTime + burnAfterReadingTime <= $now);`, { pluck: true })
501
- .all({ dialogId, now });
502
- this.database
503
- .prepare(`DELETE FROM messages
816
+ .prepare(`SELECT messageId, messageUId, json FROM messages
504
817
  WHERE dialogId = $dialogId
505
- AND burnAfterReadingFlag = 1
506
- AND (burnAfterReadingTime IS NULL OR burnAfterReadingTime = 0
507
- OR sentTime + burnAfterReadingTime <= $now);`)
508
- .run({ dialogId, now });
509
- return rows;
818
+ AND burnAfterReadingFlag = 1;`)
819
+ .all({
820
+ dialogId
821
+ });
822
+ const expiredRows = rows.filter(row => {
823
+ const message = parseMessage({ json: row.json });
824
+ return message != null && !isBurnAfterReadingMessageVisible(message, now);
825
+ });
826
+ const remove = this.database.prepare('DELETE FROM messages WHERE messageId = $messageId;');
827
+ this.database.transaction(() => {
828
+ for (const row of expiredRows) {
829
+ remove.run({ messageId: row.messageId });
830
+ }
831
+ })();
832
+ return expiredRows.flatMap(row => row.messageUId == null ? [] : [row.messageUId]);
510
833
  }
511
834
  convertToRecallMessages(messageUIds, recallMessageType) {
512
835
  const select = this.database.prepare('SELECT json FROM messages WHERE messageUId = $messageUId;');
@@ -576,6 +899,29 @@ class MessageStore {
576
899
  })
577
900
  .map(row => parseMessage(row));
578
901
  }
902
+ getLegacyMediaReferences(afterMessageId = 0, limit = 500) {
903
+ const safeCursor = Number.isSafeInteger(afterMessageId)
904
+ ? afterMessageId
905
+ : 0;
906
+ const safeLimit = Math.min(1000, Math.max(1, Math.floor(limit)));
907
+ const rows = this.database
908
+ .prepare(`SELECT messageId, json
909
+ FROM messages
910
+ WHERE messageId > $afterMessageId
911
+ ORDER BY messageId ASC
912
+ LIMIT $limit;`)
913
+ .all({
914
+ afterMessageId: safeCursor,
915
+ limit: safeLimit
916
+ });
917
+ const nextCursor = rows.length === safeLimit
918
+ ? rows[rows.length - 1]?.messageId
919
+ : undefined;
920
+ return {
921
+ items: rows.flatMap(row => extractLegacyMediaReferences(JSON.parse(row.json))),
922
+ ...(nextCursor === undefined ? {} : { nextCursor })
923
+ };
924
+ }
579
925
  getLegacyMigrationState() {
580
926
  const value = this.database
581
927
  .prepare('SELECT value FROM metadata WHERE key = $key;', { pluck: true })