LCOV - code coverage report
Current view: top level - lib/src/database - matrix_sdk_database.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 584 729 80.1 %
Date: 2024-09-30 15:57:20 Functions: 0 0 -

          Line data    Source code
       1             : /*
       2             :  *   Famedly Matrix SDK
       3             :  *   Copyright (C) 2019, 2020, 2021 Famedly GmbH
       4             :  *
       5             :  *   This program is free software: you can redistribute it and/or modify
       6             :  *   it under the terms of the GNU Affero General Public License as
       7             :  *   published by the Free Software Foundation, either version 3 of the
       8             :  *   License, or (at your option) any later version.
       9             :  *
      10             :  *   This program is distributed in the hope that it will be useful,
      11             :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12             :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13             :  *   GNU Affero General Public License for more details.
      14             :  *
      15             :  *   You should have received a copy of the GNU Affero General Public License
      16             :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17             :  */
      18             : 
      19             : import 'dart:async';
      20             : import 'dart:convert';
      21             : import 'dart:math';
      22             : 
      23             : import 'package:sqflite_common/sqflite.dart';
      24             : 
      25             : import 'package:matrix/encryption/utils/olm_session.dart';
      26             : import 'package:matrix/encryption/utils/outbound_group_session.dart';
      27             : import 'package:matrix/encryption/utils/ssss_cache.dart';
      28             : import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
      29             : import 'package:matrix/matrix.dart';
      30             : import 'package:matrix/src/utils/copy_map.dart';
      31             : import 'package:matrix/src/utils/queued_to_device_event.dart';
      32             : import 'package:matrix/src/utils/run_benchmarked.dart';
      33             : 
      34             : import 'package:matrix/src/database/indexeddb_box.dart'
      35             :     if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';
      36             : 
      37             : import 'package:matrix/src/database/database_file_storage_stub.dart'
      38             :     if (dart.library.io) 'package:matrix/src/database/database_file_storage_io.dart';
      39             : 
      40             : /// Database based on SQlite3 on native and IndexedDB on web. For native you
      41             : /// have to pass a `Database` object, which can be created with the sqflite
      42             : /// package like this:
      43             : /// ```dart
      44             : /// final database = await openDatabase('path/to/your/database');
      45             : /// ```
      46             : ///
      47             : /// **WARNING**: For android it seems like that the CursorWindow is too small for
      48             : /// large amounts of data if you are using SQFlite. Consider using a different
      49             : ///  package to open the database like
      50             : /// [sqflite_sqlcipher](https://pub.dev/packages/sqflite_sqlcipher) or
      51             : /// [sqflite_common_ffi](https://pub.dev/packages/sqflite_common_ffi).
      52             : /// Learn more at:
      53             : /// https://github.com/famedly/matrix-dart-sdk/issues/1642#issuecomment-1865827227
      54             : class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
      55             :   static const int version = 9;
      56             :   final String name;
      57             : 
      58             :   late BoxCollection _collection;
      59             :   late Box<String> _clientBox;
      60             :   late Box<Map> _accountDataBox;
      61             :   late Box<Map> _roomsBox;
      62             :   late Box<Map> _toDeviceQueueBox;
      63             : 
      64             :   /// Key is a tuple as TupleKey(roomId, type) where stateKey can be
      65             :   /// an empty string. Must contain only states of type
      66             :   /// client.importantRoomStates.
      67             :   late Box<Map> _preloadRoomStateBox;
      68             : 
      69             :   /// Key is a tuple as TupleKey(roomId, type) where stateKey can be
      70             :   /// an empty string. Must NOT contain states of a type from
      71             :   /// client.importantRoomStates.
      72             :   late Box<Map> _nonPreloadRoomStateBox;
      73             : 
      74             :   /// Key is a tuple as TupleKey(roomId, userId)
      75             :   late Box<Map> _roomMembersBox;
      76             : 
      77             :   /// Key is a tuple as TupleKey(roomId, type)
      78             :   late Box<Map> _roomAccountDataBox;
      79             :   late Box<Map> _inboundGroupSessionsBox;
      80             :   late Box<String> _inboundGroupSessionsUploadQueueBox;
      81             :   late Box<Map> _outboundGroupSessionsBox;
      82             :   late Box<Map> _olmSessionsBox;
      83             : 
      84             :   /// Key is a tuple as TupleKey(userId, deviceId)
      85             :   late Box<Map> _userDeviceKeysBox;
      86             : 
      87             :   /// Key is the user ID as a String
      88             :   late Box<bool> _userDeviceKeysOutdatedBox;
      89             : 
      90             :   /// Key is a tuple as TupleKey(userId, publicKey)
      91             :   late Box<Map> _userCrossSigningKeysBox;
      92             :   late Box<Map> _ssssCacheBox;
      93             :   late Box<Map> _presencesBox;
      94             : 
      95             :   /// Key is a tuple as Multikey(roomId, fragmentId) while the default
      96             :   /// fragmentId is an empty String
      97             :   late Box<List> _timelineFragmentsBox;
      98             : 
      99             :   /// Key is a tuple as TupleKey(roomId, eventId)
     100             :   late Box<Map> _eventsBox;
     101             : 
     102             :   /// Key is a tuple as TupleKey(userId, deviceId)
     103             :   late Box<String> _seenDeviceIdsBox;
     104             : 
     105             :   late Box<String> _seenDeviceKeysBox;
     106             : 
     107             :   late Box<Map> _userProfilesBox;
     108             : 
     109             :   @override
     110             :   final int maxFileSize;
     111             : 
     112             :   // there was a field of type `dart:io:Directory` here. This one broke the
     113             :   // dart js standalone compiler. Migration via URI as file system identifier.
     114           0 :   @Deprecated(
     115             :       'Breaks support for web standalone. Use [fileStorageLocation] instead.')
     116           0 :   Object? get fileStoragePath => fileStorageLocation?.toFilePath();
     117             : 
     118             :   static const String _clientBoxName = 'box_client';
     119             : 
     120             :   static const String _accountDataBoxName = 'box_account_data';
     121             : 
     122             :   static const String _roomsBoxName = 'box_rooms';
     123             : 
     124             :   static const String _toDeviceQueueBoxName = 'box_to_device_queue';
     125             : 
     126             :   static const String _preloadRoomStateBoxName = 'box_preload_room_states';
     127             : 
     128             :   static const String _nonPreloadRoomStateBoxName =
     129             :       'box_non_preload_room_states';
     130             : 
     131             :   static const String _roomMembersBoxName = 'box_room_members';
     132             : 
     133             :   static const String _roomAccountDataBoxName = 'box_room_account_data';
     134             : 
     135             :   static const String _inboundGroupSessionsBoxName =
     136             :       'box_inbound_group_session';
     137             : 
     138             :   static const String _inboundGroupSessionsUploadQueueBoxName =
     139             :       'box_inbound_group_sessions_upload_queue';
     140             : 
     141             :   static const String _outboundGroupSessionsBoxName =
     142             :       'box_outbound_group_session';
     143             : 
     144             :   static const String _olmSessionsBoxName = 'box_olm_session';
     145             : 
     146             :   static const String _userDeviceKeysBoxName = 'box_user_device_keys';
     147             : 
     148             :   static const String _userDeviceKeysOutdatedBoxName =
     149             :       'box_user_device_keys_outdated';
     150             : 
     151             :   static const String _userCrossSigningKeysBoxName = 'box_cross_signing_keys';
     152             : 
     153             :   static const String _ssssCacheBoxName = 'box_ssss_cache';
     154             : 
     155             :   static const String _presencesBoxName = 'box_presences';
     156             : 
     157             :   static const String _timelineFragmentsBoxName = 'box_timeline_fragments';
     158             : 
     159             :   static const String _eventsBoxName = 'box_events';
     160             : 
     161             :   static const String _seenDeviceIdsBoxName = 'box_seen_device_ids';
     162             : 
     163             :   static const String _seenDeviceKeysBoxName = 'box_seen_device_keys';
     164             : 
     165             :   static const String _userProfilesBoxName = 'box_user_profiles';
     166             : 
     167             :   Database? database;
     168             : 
     169             :   /// Custom IdbFactory used to create the indexedDB. On IO platforms it would
     170             :   /// lead to an error to import "dart:indexed_db" so this is dynamically
     171             :   /// typed.
     172             :   final dynamic idbFactory;
     173             : 
     174             :   /// Custom SQFlite Database Factory used for high level operations on IO
     175             :   /// like delete. Set it if you want to use sqlite FFI.
     176             :   final DatabaseFactory? sqfliteFactory;
     177             : 
     178          33 :   MatrixSdkDatabase(
     179             :     this.name, {
     180             :     this.database,
     181             :     this.idbFactory,
     182             :     this.sqfliteFactory,
     183             :     this.maxFileSize = 0,
     184             :     // TODO : remove deprecated member migration on next major release
     185             :     @Deprecated(
     186             :         'Breaks support for web standalone. Use [fileStorageLocation] instead.')
     187             :     dynamic fileStoragePath,
     188             :     Uri? fileStorageLocation,
     189             :     Duration? deleteFilesAfterDuration,
     190             :   }) {
     191           0 :     final legacyPath = fileStoragePath?.path;
     192          33 :     this.fileStorageLocation = fileStorageLocation ??
     193          33 :         (legacyPath is String ? Uri.tryParse(legacyPath) : null);
     194          33 :     this.deleteFilesAfterDuration = deleteFilesAfterDuration;
     195             :   }
     196             : 
     197          33 :   Future<void> open() async {
     198          66 :     _collection = await BoxCollection.open(
     199          33 :       name,
     200             :       {
     201          33 :         _clientBoxName,
     202          33 :         _accountDataBoxName,
     203          33 :         _roomsBoxName,
     204          33 :         _toDeviceQueueBoxName,
     205          33 :         _preloadRoomStateBoxName,
     206          33 :         _nonPreloadRoomStateBoxName,
     207          33 :         _roomMembersBoxName,
     208          33 :         _roomAccountDataBoxName,
     209          33 :         _inboundGroupSessionsBoxName,
     210          33 :         _inboundGroupSessionsUploadQueueBoxName,
     211          33 :         _outboundGroupSessionsBoxName,
     212          33 :         _olmSessionsBoxName,
     213          33 :         _userDeviceKeysBoxName,
     214          33 :         _userDeviceKeysOutdatedBoxName,
     215          33 :         _userCrossSigningKeysBoxName,
     216          33 :         _ssssCacheBoxName,
     217          33 :         _presencesBoxName,
     218          33 :         _timelineFragmentsBoxName,
     219          33 :         _eventsBoxName,
     220          33 :         _seenDeviceIdsBoxName,
     221          33 :         _seenDeviceKeysBoxName,
     222          33 :         _userProfilesBoxName,
     223             :       },
     224          33 :       sqfliteDatabase: database,
     225          33 :       sqfliteFactory: sqfliteFactory,
     226          33 :       idbFactory: idbFactory,
     227             :       version: version,
     228             :     );
     229          99 :     _clientBox = _collection.openBox<String>(
     230             :       _clientBoxName,
     231             :     );
     232          99 :     _accountDataBox = _collection.openBox<Map>(
     233             :       _accountDataBoxName,
     234             :     );
     235          99 :     _roomsBox = _collection.openBox<Map>(
     236             :       _roomsBoxName,
     237             :     );
     238          99 :     _preloadRoomStateBox = _collection.openBox(
     239             :       _preloadRoomStateBoxName,
     240             :     );
     241          99 :     _nonPreloadRoomStateBox = _collection.openBox(
     242             :       _nonPreloadRoomStateBoxName,
     243             :     );
     244          99 :     _roomMembersBox = _collection.openBox(
     245             :       _roomMembersBoxName,
     246             :     );
     247          99 :     _toDeviceQueueBox = _collection.openBox(
     248             :       _toDeviceQueueBoxName,
     249             :     );
     250          99 :     _roomAccountDataBox = _collection.openBox(
     251             :       _roomAccountDataBoxName,
     252             :     );
     253          99 :     _inboundGroupSessionsBox = _collection.openBox(
     254             :       _inboundGroupSessionsBoxName,
     255             :     );
     256          99 :     _inboundGroupSessionsUploadQueueBox = _collection.openBox(
     257             :       _inboundGroupSessionsUploadQueueBoxName,
     258             :     );
     259          99 :     _outboundGroupSessionsBox = _collection.openBox(
     260             :       _outboundGroupSessionsBoxName,
     261             :     );
     262          99 :     _olmSessionsBox = _collection.openBox(
     263             :       _olmSessionsBoxName,
     264             :     );
     265          99 :     _userDeviceKeysBox = _collection.openBox(
     266             :       _userDeviceKeysBoxName,
     267             :     );
     268          99 :     _userDeviceKeysOutdatedBox = _collection.openBox(
     269             :       _userDeviceKeysOutdatedBoxName,
     270             :     );
     271          99 :     _userCrossSigningKeysBox = _collection.openBox(
     272             :       _userCrossSigningKeysBoxName,
     273             :     );
     274          99 :     _ssssCacheBox = _collection.openBox(
     275             :       _ssssCacheBoxName,
     276             :     );
     277          99 :     _presencesBox = _collection.openBox(
     278             :       _presencesBoxName,
     279             :     );
     280          99 :     _timelineFragmentsBox = _collection.openBox(
     281             :       _timelineFragmentsBoxName,
     282             :     );
     283          99 :     _eventsBox = _collection.openBox(
     284             :       _eventsBoxName,
     285             :     );
     286          99 :     _seenDeviceIdsBox = _collection.openBox(
     287             :       _seenDeviceIdsBoxName,
     288             :     );
     289          99 :     _seenDeviceKeysBox = _collection.openBox(
     290             :       _seenDeviceKeysBoxName,
     291             :     );
     292          99 :     _userProfilesBox = _collection.openBox(
     293             :       _userProfilesBoxName,
     294             :     );
     295             : 
     296             :     // Check version and check if we need a migration
     297          99 :     final currentVersion = int.tryParse(await _clientBox.get('version') ?? '');
     298             :     if (currentVersion == null) {
     299          99 :       await _clientBox.put('version', version.toString());
     300           0 :     } else if (currentVersion != version) {
     301           0 :       await _migrateFromVersion(currentVersion);
     302             :     }
     303             : 
     304             :     return;
     305             :   }
     306             : 
     307           0 :   Future<void> _migrateFromVersion(int currentVersion) async {
     308           0 :     Logs().i('Migrate store database from version $currentVersion to $version');
     309             : 
     310           0 :     if (version == 8) {
     311             :       // Migrate to inbound group sessions upload queue:
     312           0 :       final allInboundGroupSessions = await getAllInboundGroupSessions();
     313             :       final sessionsToUpload = allInboundGroupSessions
     314             :           // ignore: deprecated_member_use_from_same_package
     315           0 :           .where((session) => session.uploaded == false)
     316           0 :           .toList();
     317           0 :       Logs().i(
     318           0 :           'Move ${allInboundGroupSessions.length} inbound group sessions to upload to their own queue...');
     319           0 :       await transaction(() async {
     320           0 :         for (final session in sessionsToUpload) {
     321           0 :           await _inboundGroupSessionsUploadQueueBox.put(
     322           0 :             session.sessionId,
     323           0 :             session.roomId,
     324             :           );
     325             :         }
     326             :       });
     327           0 :       if (currentVersion == 7) {
     328           0 :         await _clientBox.put('version', version.toString());
     329             :         return;
     330             :       }
     331             :     }
     332             :     // The default version upgrade:
     333           0 :     await clearCache();
     334           0 :     await _clientBox.put('version', version.toString());
     335             :   }
     336             : 
     337           9 :   @override
     338          18 :   Future<void> clear() => _collection.clear();
     339             : 
     340           3 :   @override
     341           6 :   Future<void> clearCache() => transaction(() async {
     342           6 :         await _roomsBox.clear();
     343           6 :         await _accountDataBox.clear();
     344           6 :         await _roomAccountDataBox.clear();
     345           6 :         await _preloadRoomStateBox.clear();
     346           6 :         await _nonPreloadRoomStateBox.clear();
     347           6 :         await _roomMembersBox.clear();
     348           6 :         await _eventsBox.clear();
     349           6 :         await _timelineFragmentsBox.clear();
     350           6 :         await _outboundGroupSessionsBox.clear();
     351           6 :         await _presencesBox.clear();
     352           6 :         await _userProfilesBox.clear();
     353           6 :         await _clientBox.delete('prev_batch');
     354             :       });
     355             : 
     356           4 :   @override
     357           8 :   Future<void> clearSSSSCache() => _ssssCacheBox.clear();
     358             : 
     359          20 :   @override
     360          40 :   Future<void> close() async => _collection.close();
     361             : 
     362           2 :   @override
     363             :   Future<void> deleteFromToDeviceQueue(int id) async {
     364           6 :     await _toDeviceQueueBox.delete(id.toString());
     365             :     return;
     366             :   }
     367             : 
     368          31 :   @override
     369             :   Future<void> forgetRoom(String roomId) async {
     370         124 :     await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
     371          62 :     final eventsBoxKeys = await _eventsBox.getAllKeys();
     372          31 :     for (final key in eventsBoxKeys) {
     373           0 :       final multiKey = TupleKey.fromString(key);
     374           0 :       if (multiKey.parts.first != roomId) continue;
     375           0 :       await _eventsBox.delete(key);
     376             :     }
     377          62 :     final preloadRoomStateBoxKeys = await _preloadRoomStateBox.getAllKeys();
     378          33 :     for (final key in preloadRoomStateBoxKeys) {
     379           2 :       final multiKey = TupleKey.fromString(key);
     380           6 :       if (multiKey.parts.first != roomId) continue;
     381           0 :       await _preloadRoomStateBox.delete(key);
     382             :     }
     383             :     final nonPreloadRoomStateBoxKeys =
     384          62 :         await _nonPreloadRoomStateBox.getAllKeys();
     385          31 :     for (final key in nonPreloadRoomStateBoxKeys) {
     386           0 :       final multiKey = TupleKey.fromString(key);
     387           0 :       if (multiKey.parts.first != roomId) continue;
     388           0 :       await _nonPreloadRoomStateBox.delete(key);
     389             :     }
     390          62 :     final roomMembersBoxKeys = await _roomMembersBox.getAllKeys();
     391          33 :     for (final key in roomMembersBoxKeys) {
     392           2 :       final multiKey = TupleKey.fromString(key);
     393           6 :       if (multiKey.parts.first != roomId) continue;
     394           0 :       await _roomMembersBox.delete(key);
     395             :     }
     396          62 :     final roomAccountDataBoxKeys = await _roomAccountDataBox.getAllKeys();
     397          31 :     for (final key in roomAccountDataBoxKeys) {
     398           0 :       final multiKey = TupleKey.fromString(key);
     399           0 :       if (multiKey.parts.first != roomId) continue;
     400           0 :       await _roomAccountDataBox.delete(key);
     401             :     }
     402          62 :     await _roomsBox.delete(roomId);
     403             :   }
     404             : 
     405          31 :   @override
     406             :   Future<Map<String, BasicEvent>> getAccountData() =>
     407          31 :       runBenchmarked<Map<String, BasicEvent>>('Get all account data from store',
     408          31 :           () async {
     409          31 :         final accountData = <String, BasicEvent>{};
     410          62 :         final raws = await _accountDataBox.getAllValues();
     411          33 :         for (final entry in raws.entries) {
     412           6 :           accountData[entry.key] = BasicEvent(
     413           2 :             type: entry.key,
     414           4 :             content: copyMap(entry.value),
     415             :           );
     416             :         }
     417             :         return accountData;
     418             :       });
     419             : 
     420          31 :   @override
     421             :   Future<Map<String, dynamic>?> getClient(String name) =>
     422          62 :       runBenchmarked('Get Client from store', () async {
     423          31 :         final map = <String, dynamic>{};
     424          62 :         final keys = await _clientBox.getAllKeys();
     425          62 :         for (final key in keys) {
     426          31 :           if (key == 'version') continue;
     427           4 :           final value = await _clientBox.get(key);
     428           2 :           if (value != null) map[key] = value;
     429             :         }
     430          31 :         if (map.isEmpty) return null;
     431             :         return map;
     432             :       });
     433             : 
     434           8 :   @override
     435             :   Future<Event?> getEventById(String eventId, Room room) async {
     436          40 :     final raw = await _eventsBox.get(TupleKey(room.id, eventId).toString());
     437             :     if (raw == null) return null;
     438          12 :     return Event.fromJson(copyMap(raw), room);
     439             :   }
     440             : 
     441             :   /// Loads a whole list of events at once from the store for a specific room
     442           6 :   Future<List<Event>> _getEventsByIds(List<String> eventIds, Room room) async {
     443             :     final keys = eventIds
     444           6 :         .map(
     445          12 :           (eventId) => TupleKey(room.id, eventId).toString(),
     446             :         )
     447           6 :         .toList();
     448          12 :     final rawEvents = await _eventsBox.getAll(keys);
     449             :     return rawEvents
     450           6 :         .whereType<Map>()
     451          15 :         .map((rawEvent) => Event.fromJson(copyMap(rawEvent), room))
     452           6 :         .toList();
     453             :   }
     454             : 
     455           6 :   @override
     456             :   Future<List<Event>> getEventList(
     457             :     Room room, {
     458             :     int start = 0,
     459             :     bool onlySending = false,
     460             :     int? limit,
     461             :   }) =>
     462          12 :       runBenchmarked<List<Event>>('Get event list', () async {
     463             :         // Get the synced event IDs from the store
     464          18 :         final timelineKey = TupleKey(room.id, '').toString();
     465             :         final timelineEventIds =
     466          15 :             (await _timelineFragmentsBox.get(timelineKey) ?? []);
     467             : 
     468             :         // Get the local stored SENDING events from the store
     469             :         late final List sendingEventIds;
     470           6 :         if (start != 0) {
     471           2 :           sendingEventIds = [];
     472             :         } else {
     473          18 :           final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
     474             :           sendingEventIds =
     475          16 :               (await _timelineFragmentsBox.get(sendingTimelineKey) ?? []);
     476             :         }
     477             : 
     478             :         // Combine those two lists while respecting the start and limit parameters.
     479           9 :         final end = min(timelineEventIds.length,
     480           8 :             start + (limit ?? timelineEventIds.length));
     481           6 :         final eventIds = [
     482             :           ...sendingEventIds,
     483          10 :           if (!onlySending && start < timelineEventIds.length)
     484           3 :             ...timelineEventIds.getRange(start, end),
     485             :         ];
     486             : 
     487          12 :         return await _getEventsByIds(eventIds.cast<String>(), room);
     488             :       });
     489             : 
     490          11 :   @override
     491             :   Future<StoredInboundGroupSession?> getInboundGroupSession(
     492             :     String roomId,
     493             :     String sessionId,
     494             :   ) async {
     495          22 :     final raw = await _inboundGroupSessionsBox.get(sessionId);
     496             :     if (raw == null) return null;
     497          16 :     return StoredInboundGroupSession.fromJson(copyMap(raw));
     498             :   }
     499             : 
     500           6 :   @override
     501             :   Future<List<StoredInboundGroupSession>>
     502             :       getInboundGroupSessionsToUpload() async {
     503             :     final uploadQueue =
     504          12 :         await _inboundGroupSessionsUploadQueueBox.getAllValues();
     505           6 :     final sessionFutures = uploadQueue.entries
     506           6 :         .take(50)
     507          26 :         .map((entry) => getInboundGroupSession(entry.value, entry.key));
     508           6 :     final sessions = await Future.wait(sessionFutures);
     509          12 :     return sessions.whereType<StoredInboundGroupSession>().toList();
     510             :   }
     511             : 
     512           2 :   @override
     513             :   Future<List<String>> getLastSentMessageUserDeviceKey(
     514             :       String userId, String deviceId) async {
     515             :     final raw =
     516           8 :         await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString());
     517           1 :     if (raw == null) return <String>[];
     518           2 :     return <String>[raw['last_sent_message']];
     519             :   }
     520             : 
     521          24 :   @override
     522             :   Future<void> storeOlmSession(String identityKey, String sessionId,
     523             :       String pickle, int lastReceived) async {
     524          96 :     final rawSessions = copyMap((await _olmSessionsBox.get(identityKey)) ?? {});
     525          48 :     rawSessions[sessionId] = {
     526             :       'identity_key': identityKey,
     527             :       'pickle': pickle,
     528             :       'session_id': sessionId,
     529             :       'last_received': lastReceived,
     530             :     };
     531          48 :     await _olmSessionsBox.put(identityKey, rawSessions);
     532             :     return;
     533             :   }
     534             : 
     535          24 :   @override
     536             :   Future<List<OlmSession>> getOlmSessions(
     537             :       String identityKey, String userId) async {
     538          48 :     final rawSessions = await _olmSessionsBox.get(identityKey);
     539          29 :     if (rawSessions == null || rawSessions.isEmpty) return <OlmSession>[];
     540           5 :     return rawSessions.values
     541          20 :         .map((json) => OlmSession.fromJson(copyMap(json), userId))
     542           5 :         .toList();
     543             :   }
     544             : 
     545           2 :   @override
     546             :   Future<Map<String, Map>> getAllOlmSessions() =>
     547           4 :       _olmSessionsBox.getAllValues();
     548             : 
     549          11 :   @override
     550             :   Future<List<OlmSession>> getOlmSessionsForDevices(
     551             :       List<String> identityKeys, String userId) async {
     552          11 :     final sessions = await Future.wait(
     553          33 :         identityKeys.map((identityKey) => getOlmSessions(identityKey, userId)));
     554          33 :     return <OlmSession>[for (final sublist in sessions) ...sublist];
     555             :   }
     556             : 
     557           4 :   @override
     558             :   Future<OutboundGroupSession?> getOutboundGroupSession(
     559             :       String roomId, String userId) async {
     560           8 :     final raw = await _outboundGroupSessionsBox.get(roomId);
     561             :     if (raw == null) return null;
     562           4 :     return OutboundGroupSession.fromJson(copyMap(raw), userId);
     563             :   }
     564             : 
     565           4 :   @override
     566             :   Future<Room?> getSingleRoom(Client client, String roomId,
     567             :       {bool loadImportantStates = true}) async {
     568             :     // Get raw room from database:
     569           8 :     final roomData = await _roomsBox.get(roomId);
     570             :     if (roomData == null) return null;
     571           8 :     final room = Room.fromJson(copyMap(roomData), client);
     572             : 
     573             :     // Get important states:
     574             :     if (loadImportantStates) {
     575           4 :       final dbKeys = client.importantStateEvents
     576          16 :           .map((state) => TupleKey(roomId, state).toString())
     577           4 :           .toList();
     578           8 :       final rawStates = await _preloadRoomStateBox.getAll(dbKeys);
     579           8 :       for (final rawState in rawStates) {
     580           2 :         if (rawState == null || rawState[''] == null) continue;
     581           8 :         room.setState(Event.fromJson(copyMap(rawState['']), room));
     582             :       }
     583             :     }
     584             : 
     585             :     return room;
     586             :   }
     587             : 
     588          31 :   @override
     589             :   Future<List<Room>> getRoomList(Client client) =>
     590          62 :       runBenchmarked<List<Room>>('Get room list from store', () async {
     591          31 :         final rooms = <String, Room>{};
     592             : 
     593          62 :         final rawRooms = await _roomsBox.getAllValues();
     594             : 
     595          33 :         for (final raw in rawRooms.values) {
     596             :           // Get the room
     597           4 :           final room = Room.fromJson(copyMap(raw), client);
     598             : 
     599             :           // Add to the list and continue.
     600           4 :           rooms[room.id] = room;
     601             :         }
     602             : 
     603          62 :         final roomStatesDataRaws = await _preloadRoomStateBox.getAllValues();
     604          32 :         for (final entry in roomStatesDataRaws.entries) {
     605           2 :           final keys = TupleKey.fromString(entry.key);
     606           2 :           final roomId = keys.parts.first;
     607           1 :           final room = rooms[roomId];
     608             :           if (room == null) {
     609           0 :             Logs().w('Found event in store for unknown room', entry.value);
     610             :             continue;
     611             :           }
     612           1 :           final states = entry.value;
     613           1 :           final stateEvents = states.values
     614           4 :               .map((raw) => room.membership == Membership.invite
     615           2 :                   ? StrippedStateEvent.fromJson(copyMap(raw))
     616           2 :                   : Event.fromJson(copyMap(raw), room))
     617           1 :               .toList();
     618           2 :           for (final state in stateEvents) {
     619           1 :             room.setState(state);
     620             :           }
     621             :         }
     622             : 
     623             :         // Get the room account data
     624          62 :         final roomAccountDataRaws = await _roomAccountDataBox.getAllValues();
     625          32 :         for (final entry in roomAccountDataRaws.entries) {
     626           2 :           final keys = TupleKey.fromString(entry.key);
     627           1 :           final basicRoomEvent = BasicRoomEvent.fromJson(
     628           2 :             copyMap(entry.value),
     629             :           );
     630           2 :           final roomId = keys.parts.first;
     631           1 :           if (rooms.containsKey(roomId)) {
     632           4 :             rooms[roomId]!.roomAccountData[basicRoomEvent.type] =
     633             :                 basicRoomEvent;
     634             :           } else {
     635           0 :             Logs().w(
     636           0 :                 'Found account data for unknown room $roomId. Delete now...');
     637           0 :             await _roomAccountDataBox
     638           0 :                 .delete(TupleKey(roomId, basicRoomEvent.type).toString());
     639             :           }
     640             :         }
     641             : 
     642          62 :         return rooms.values.toList();
     643             :       });
     644             : 
     645          24 :   @override
     646             :   Future<SSSSCache?> getSSSSCache(String type) async {
     647          48 :     final raw = await _ssssCacheBox.get(type);
     648             :     if (raw == null) return null;
     649          16 :     return SSSSCache.fromJson(copyMap(raw));
     650             :   }
     651             : 
     652          31 :   @override
     653             :   Future<List<QueuedToDeviceEvent>> getToDeviceEventQueue() async {
     654          62 :     final raws = await _toDeviceQueueBox.getAllValues();
     655          64 :     final copiedRaws = raws.entries.map((entry) {
     656           4 :       final copiedRaw = copyMap(entry.value);
     657           6 :       copiedRaw['id'] = int.parse(entry.key);
     658           6 :       copiedRaw['content'] = jsonDecode(copiedRaw['content'] as String);
     659             :       return copiedRaw;
     660          31 :     }).toList();
     661          66 :     return copiedRaws.map((raw) => QueuedToDeviceEvent.fromJson(raw)).toList();
     662             :   }
     663             : 
     664           6 :   @override
     665             :   Future<List<Event>> getUnimportantRoomEventStatesForRoom(
     666             :       List<String> events, Room room) async {
     667          21 :     final keys = (await _nonPreloadRoomStateBox.getAllKeys()).where((key) {
     668           3 :       final tuple = TupleKey.fromString(key);
     669          21 :       return tuple.parts.first == room.id && !events.contains(tuple.parts[1]);
     670             :     });
     671             : 
     672           6 :     final unimportantEvents = <Event>[];
     673           9 :     for (final key in keys) {
     674           6 :       final states = await _nonPreloadRoomStateBox.get(key);
     675             :       if (states == null) continue;
     676           3 :       unimportantEvents.addAll(
     677          15 :           states.values.map((raw) => Event.fromJson(copyMap(raw), room)));
     678             :     }
     679             : 
     680          18 :     return unimportantEvents.where((event) => event.stateKey != null).toList();
     681             :   }
     682             : 
     683          31 :   @override
     684             :   Future<User?> getUser(String userId, Room room) async {
     685             :     final state =
     686         155 :         await _roomMembersBox.get(TupleKey(room.id, userId).toString());
     687             :     if (state == null) return null;
     688          90 :     return Event.fromJson(copyMap(state), room).asUser;
     689             :   }
     690             : 
     691          31 :   @override
     692             :   Future<Map<String, DeviceKeysList>> getUserDeviceKeys(Client client) =>
     693          31 :       runBenchmarked<Map<String, DeviceKeysList>>(
     694          31 :           'Get all user device keys from store', () async {
     695             :         final deviceKeysOutdated =
     696          62 :             await _userDeviceKeysOutdatedBox.getAllValues();
     697          31 :         if (deviceKeysOutdated.isEmpty) {
     698          31 :           return {};
     699             :         }
     700           1 :         final res = <String, DeviceKeysList>{};
     701           2 :         final userDeviceKeys = await _userDeviceKeysBox.getAllValues();
     702             :         final userCrossSigningKeys =
     703           2 :             await _userCrossSigningKeysBox.getAllValues();
     704           2 :         for (final userId in deviceKeysOutdated.keys) {
     705           3 :           final deviceKeysBoxKeys = userDeviceKeys.keys.where((tuple) {
     706           1 :             final tupleKey = TupleKey.fromString(tuple);
     707           3 :             return tupleKey.parts.first == userId;
     708             :           });
     709             :           final crossSigningKeysBoxKeys =
     710           3 :               userCrossSigningKeys.keys.where((tuple) {
     711           1 :             final tupleKey = TupleKey.fromString(tuple);
     712           3 :             return tupleKey.parts.first == userId;
     713             :           });
     714           1 :           final childEntries = deviceKeysBoxKeys.map(
     715           1 :             (key) {
     716           1 :               final userDeviceKey = userDeviceKeys[key];
     717             :               if (userDeviceKey == null) return null;
     718           1 :               return copyMap(userDeviceKey);
     719             :             },
     720             :           );
     721           1 :           final crossSigningEntries = crossSigningKeysBoxKeys.map(
     722           1 :             (key) {
     723           1 :               final crossSigningKey = userCrossSigningKeys[key];
     724             :               if (crossSigningKey == null) return null;
     725           1 :               return copyMap(crossSigningKey);
     726             :             },
     727             :           );
     728           2 :           res[userId] = DeviceKeysList.fromDbJson(
     729           1 :               {
     730           1 :                 'client_id': client.id,
     731             :                 'user_id': userId,
     732           1 :                 'outdated': deviceKeysOutdated[userId],
     733             :               },
     734             :               childEntries
     735           2 :                   .where((c) => c != null)
     736           1 :                   .toList()
     737           1 :                   .cast<Map<String, dynamic>>(),
     738             :               crossSigningEntries
     739           2 :                   .where((c) => c != null)
     740           1 :                   .toList()
     741           1 :                   .cast<Map<String, dynamic>>(),
     742             :               client);
     743             :         }
     744             :         return res;
     745             :       });
     746             : 
     747          31 :   @override
     748             :   Future<List<User>> getUsers(Room room) async {
     749          31 :     final users = <User>[];
     750          62 :     final keys = (await _roomMembersBox.getAllKeys())
     751         211 :         .where((key) => TupleKey.fromString(key).parts.first == room.id)
     752          31 :         .toList();
     753          62 :     final states = await _roomMembersBox.getAll(keys);
     754          32 :     states.removeWhere((state) => state == null);
     755          32 :     for (final state in states) {
     756           4 :       users.add(Event.fromJson(copyMap(state!), room).asUser);
     757             :     }
     758             : 
     759             :     return users;
     760             :   }
     761             : 
     762          33 :   @override
     763             :   Future<int> insertClient(
     764             :       String name,
     765             :       String homeserverUrl,
     766             :       String token,
     767             :       DateTime? tokenExpiresAt,
     768             :       String? refreshToken,
     769             :       String userId,
     770             :       String? deviceId,
     771             :       String? deviceName,
     772             :       String? prevBatch,
     773             :       String? olmAccount) async {
     774          66 :     await transaction(() async {
     775          66 :       await _clientBox.put('homeserver_url', homeserverUrl);
     776          66 :       await _clientBox.put('token', token);
     777             :       if (tokenExpiresAt == null) {
     778          64 :         await _clientBox.delete('token_expires_at');
     779             :       } else {
     780           2 :         await _clientBox.put('token_expires_at',
     781           2 :             tokenExpiresAt.millisecondsSinceEpoch.toString());
     782             :       }
     783             :       if (refreshToken == null) {
     784          12 :         await _clientBox.delete('refresh_token');
     785             :       } else {
     786          62 :         await _clientBox.put('refresh_token', refreshToken);
     787             :       }
     788          66 :       await _clientBox.put('user_id', userId);
     789             :       if (deviceId == null) {
     790           4 :         await _clientBox.delete('device_id');
     791             :       } else {
     792          62 :         await _clientBox.put('device_id', deviceId);
     793             :       }
     794             :       if (deviceName == null) {
     795           4 :         await _clientBox.delete('device_name');
     796             :       } else {
     797          62 :         await _clientBox.put('device_name', deviceName);
     798             :       }
     799             :       if (prevBatch == null) {
     800          64 :         await _clientBox.delete('prev_batch');
     801             :       } else {
     802           4 :         await _clientBox.put('prev_batch', prevBatch);
     803             :       }
     804             :       if (olmAccount == null) {
     805          18 :         await _clientBox.delete('olm_account');
     806             :       } else {
     807          48 :         await _clientBox.put('olm_account', olmAccount);
     808             :       }
     809          66 :       await _clientBox.delete('sync_filter_id');
     810             :     });
     811             :     return 0;
     812             :   }
     813             : 
     814           2 :   @override
     815             :   Future<int> insertIntoToDeviceQueue(
     816             :       String type, String txnId, String content) async {
     817           4 :     final id = DateTime.now().millisecondsSinceEpoch;
     818           8 :     await _toDeviceQueueBox.put(id.toString(), {
     819             :       'type': type,
     820             :       'txn_id': txnId,
     821             :       'content': content,
     822             :     });
     823             :     return id;
     824             :   }
     825             : 
     826           5 :   @override
     827             :   Future<void> markInboundGroupSessionAsUploaded(
     828             :       String roomId, String sessionId) async {
     829          10 :     await _inboundGroupSessionsUploadQueueBox.delete(sessionId);
     830             :     return;
     831             :   }
     832             : 
     833           2 :   @override
     834             :   Future<void> markInboundGroupSessionsAsNeedingUpload() async {
     835           4 :     final keys = await _inboundGroupSessionsBox.getAllKeys();
     836           4 :     for (final sessionId in keys) {
     837           2 :       final raw = copyMap(
     838           4 :         await _inboundGroupSessionsBox.get(sessionId) ?? {},
     839             :       );
     840           2 :       if (raw.isEmpty) continue;
     841           2 :       final roomId = raw.tryGet<String>('room_id');
     842             :       if (roomId == null) continue;
     843           4 :       await _inboundGroupSessionsUploadQueueBox.put(sessionId, roomId);
     844             :     }
     845             :     return;
     846             :   }
     847             : 
     848          10 :   @override
     849             :   Future<void> removeEvent(String eventId, String roomId) async {
     850          40 :     await _eventsBox.delete(TupleKey(roomId, eventId).toString());
     851          20 :     final keys = await _timelineFragmentsBox.getAllKeys();
     852          20 :     for (final key in keys) {
     853          10 :       final multiKey = TupleKey.fromString(key);
     854          30 :       if (multiKey.parts.first != roomId) continue;
     855             :       final eventIds =
     856          30 :           List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
     857          10 :       final prevLength = eventIds.length;
     858          30 :       eventIds.removeWhere((id) => id == eventId);
     859          20 :       if (eventIds.length < prevLength) {
     860          20 :         await _timelineFragmentsBox.put(key, eventIds);
     861             :       }
     862             :     }
     863             :     return;
     864             :   }
     865             : 
     866           2 :   @override
     867             :   Future<void> removeOutboundGroupSession(String roomId) async {
     868           4 :     await _outboundGroupSessionsBox.delete(roomId);
     869             :     return;
     870             :   }
     871             : 
     872           4 :   @override
     873             :   Future<void> removeUserCrossSigningKey(
     874             :       String userId, String publicKey) async {
     875           4 :     await _userCrossSigningKeysBox
     876          12 :         .delete(TupleKey(userId, publicKey).toString());
     877             :     return;
     878             :   }
     879             : 
     880           1 :   @override
     881             :   Future<void> removeUserDeviceKey(String userId, String deviceId) async {
     882           4 :     await _userDeviceKeysBox.delete(TupleKey(userId, deviceId).toString());
     883             :     return;
     884             :   }
     885             : 
     886           3 :   @override
     887             :   Future<void> setBlockedUserCrossSigningKey(
     888             :       bool blocked, String userId, String publicKey) async {
     889           3 :     final raw = copyMap(
     890           3 :       await _userCrossSigningKeysBox
     891           9 :               .get(TupleKey(userId, publicKey).toString()) ??
     892           0 :           {},
     893             :     );
     894           3 :     raw['blocked'] = blocked;
     895           6 :     await _userCrossSigningKeysBox.put(
     896           6 :       TupleKey(userId, publicKey).toString(),
     897             :       raw,
     898             :     );
     899             :     return;
     900             :   }
     901             : 
     902           3 :   @override
     903             :   Future<void> setBlockedUserDeviceKey(
     904             :       bool blocked, String userId, String deviceId) async {
     905           3 :     final raw = copyMap(
     906          12 :       await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
     907             :     );
     908           3 :     raw['blocked'] = blocked;
     909           6 :     await _userDeviceKeysBox.put(
     910           6 :       TupleKey(userId, deviceId).toString(),
     911             :       raw,
     912             :     );
     913             :     return;
     914             :   }
     915             : 
     916           1 :   @override
     917             :   Future<void> setLastActiveUserDeviceKey(
     918             :       int lastActive, String userId, String deviceId) async {
     919           1 :     final raw = copyMap(
     920           4 :       await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
     921             :     );
     922             : 
     923           1 :     raw['last_active'] = lastActive;
     924           2 :     await _userDeviceKeysBox.put(
     925           2 :       TupleKey(userId, deviceId).toString(),
     926             :       raw,
     927             :     );
     928             :   }
     929             : 
     930           7 :   @override
     931             :   Future<void> setLastSentMessageUserDeviceKey(
     932             :       String lastSentMessage, String userId, String deviceId) async {
     933           7 :     final raw = copyMap(
     934          28 :       await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
     935             :     );
     936           7 :     raw['last_sent_message'] = lastSentMessage;
     937          14 :     await _userDeviceKeysBox.put(
     938          14 :       TupleKey(userId, deviceId).toString(),
     939             :       raw,
     940             :     );
     941             :   }
     942             : 
     943           2 :   @override
     944             :   Future<void> setRoomPrevBatch(
     945             :       String? prevBatch, String roomId, Client client) async {
     946           4 :     final raw = await _roomsBox.get(roomId);
     947             :     if (raw == null) return;
     948           2 :     final room = Room.fromJson(copyMap(raw), client);
     949           1 :     room.prev_batch = prevBatch;
     950           3 :     await _roomsBox.put(roomId, room.toJson());
     951             :     return;
     952             :   }
     953             : 
     954           6 :   @override
     955             :   Future<void> setVerifiedUserCrossSigningKey(
     956             :       bool verified, String userId, String publicKey) async {
     957           6 :     final raw = copyMap(
     958           6 :       (await _userCrossSigningKeysBox
     959          18 :               .get(TupleKey(userId, publicKey).toString())) ??
     960           1 :           {},
     961             :     );
     962           6 :     raw['verified'] = verified;
     963          12 :     await _userCrossSigningKeysBox.put(
     964          12 :       TupleKey(userId, publicKey).toString(),
     965             :       raw,
     966             :     );
     967             :     return;
     968             :   }
     969             : 
     970           4 :   @override
     971             :   Future<void> setVerifiedUserDeviceKey(
     972             :       bool verified, String userId, String deviceId) async {
     973           4 :     final raw = copyMap(
     974          16 :       await _userDeviceKeysBox.get(TupleKey(userId, deviceId).toString()) ?? {},
     975             :     );
     976           4 :     raw['verified'] = verified;
     977           8 :     await _userDeviceKeysBox.put(
     978           8 :       TupleKey(userId, deviceId).toString(),
     979             :       raw,
     980             :     );
     981             :     return;
     982             :   }
     983             : 
     984          31 :   @override
     985             :   Future<void> storeAccountData(String type, String content) async {
     986          93 :     await _accountDataBox.put(type, jsonDecode(content));
     987             :     return;
     988             :   }
     989             : 
     990          33 :   @override
     991             :   Future<void> storeEventUpdate(EventUpdate eventUpdate, Client client) async {
     992             :     // Ephemerals should not be stored
     993          66 :     if (eventUpdate.type == EventUpdateType.ephemeral) return;
     994          66 :     final tmpRoom = client.getRoomById(eventUpdate.roomID) ??
     995          12 :         Room(id: eventUpdate.roomID, client: client);
     996             : 
     997             :     // In case of this is a redaction event
     998          99 :     if (eventUpdate.content['type'] == EventTypes.Redaction) {
     999           4 :       final eventId = eventUpdate.content.tryGet<String>('redacts');
    1000             :       final event =
    1001           0 :           eventId != null ? await getEventById(eventId, tmpRoom) : null;
    1002             :       if (event != null) {
    1003           0 :         event.setRedactionEvent(Event.fromJson(eventUpdate.content, tmpRoom));
    1004           0 :         await _eventsBox.put(
    1005           0 :             TupleKey(eventUpdate.roomID, event.eventId).toString(),
    1006           0 :             event.toJson());
    1007             : 
    1008           0 :         if (tmpRoom.lastEvent?.eventId == event.eventId) {
    1009           0 :           if (client.importantStateEvents.contains(event.type)) {
    1010           0 :             await _preloadRoomStateBox.put(
    1011           0 :               TupleKey(eventUpdate.roomID, event.type).toString(),
    1012           0 :               {'': event.toJson()},
    1013             :             );
    1014             :           } else {
    1015           0 :             await _nonPreloadRoomStateBox.put(
    1016           0 :               TupleKey(eventUpdate.roomID, event.type).toString(),
    1017           0 :               {'': event.toJson()},
    1018             :             );
    1019             :           }
    1020             :         }
    1021             :       }
    1022             :     }
    1023             : 
    1024             :     // Store a common message event
    1025          66 :     if ({EventUpdateType.timeline, EventUpdateType.history}
    1026          66 :         .contains(eventUpdate.type)) {
    1027          66 :       final eventId = eventUpdate.content['event_id'];
    1028             :       // Is this ID already in the store?
    1029          33 :       final prevEvent = await _eventsBox
    1030         132 :           .get(TupleKey(eventUpdate.roomID, eventId).toString());
    1031             :       final prevStatus = prevEvent == null
    1032             :           ? null
    1033           9 :           : () {
    1034           9 :               final json = copyMap(prevEvent);
    1035           9 :               final statusInt = json.tryGet<int>('status') ??
    1036             :                   json
    1037           0 :                       .tryGetMap<String, dynamic>('unsigned')
    1038           0 :                       ?.tryGet<int>(messageSendingStatusKey);
    1039           9 :               return statusInt == null ? null : eventStatusFromInt(statusInt);
    1040           9 :             }();
    1041             : 
    1042             :       // calculate the status
    1043          33 :       final newStatus = eventStatusFromInt(
    1044          66 :         eventUpdate.content.tryGet<int>('status') ??
    1045          33 :             eventUpdate.content
    1046          33 :                 .tryGetMap<String, dynamic>('unsigned')
    1047          30 :                 ?.tryGet<int>(messageSendingStatusKey) ??
    1048          33 :             EventStatus.synced.intValue,
    1049             :       );
    1050             : 
    1051             :       // Is this the response to a sending event which is already synced? Then
    1052             :       // there is nothing to do here.
    1053          39 :       if (!newStatus.isSynced && prevStatus != null && prevStatus.isSynced) {
    1054             :         return;
    1055             :       }
    1056             : 
    1057          33 :       final status = newStatus.isError || prevStatus == null
    1058             :           ? newStatus
    1059           7 :           : latestEventStatus(
    1060             :               prevStatus,
    1061             :               newStatus,
    1062             :             );
    1063             : 
    1064             :       // Add the status and the sort order to the content so it get stored
    1065          99 :       eventUpdate.content['unsigned'] ??= <String, dynamic>{};
    1066          99 :       eventUpdate.content['unsigned'][messageSendingStatusKey] =
    1067          99 :           eventUpdate.content['status'] = status.intValue;
    1068             : 
    1069             :       // In case this event has sent from this account we have a transaction ID
    1070          33 :       final transactionId = eventUpdate.content
    1071          33 :           .tryGetMap<String, dynamic>('unsigned')
    1072          33 :           ?.tryGet<String>('transaction_id');
    1073         165 :       await _eventsBox.put(TupleKey(eventUpdate.roomID, eventId).toString(),
    1074          33 :           eventUpdate.content);
    1075             : 
    1076             :       // Update timeline fragments
    1077          99 :       final key = TupleKey(eventUpdate.roomID, status.isSent ? '' : 'SENDING')
    1078          33 :           .toString();
    1079             : 
    1080             :       final eventIds =
    1081         132 :           List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
    1082             : 
    1083          33 :       if (!eventIds.contains(eventId)) {
    1084          66 :         if (eventUpdate.type == EventUpdateType.history) {
    1085           2 :           eventIds.add(eventId);
    1086             :         } else {
    1087          33 :           eventIds.insert(0, eventId);
    1088             :         }
    1089          66 :         await _timelineFragmentsBox.put(key, eventIds);
    1090           7 :       } else if (status.isSynced &&
    1091             :           prevStatus != null &&
    1092           5 :           prevStatus.isSent &&
    1093          10 :           eventUpdate.type != EventUpdateType.history) {
    1094             :         // Status changes from 1 -> 2? Make sure event is correctly sorted.
    1095           5 :         eventIds.remove(eventId);
    1096           5 :         eventIds.insert(0, eventId);
    1097             :       }
    1098             : 
    1099             :       // If event comes from server timeline, remove sending events with this ID
    1100          33 :       if (status.isSent) {
    1101          99 :         final key = TupleKey(eventUpdate.roomID, 'SENDING').toString();
    1102             :         final eventIds =
    1103         132 :             List<String>.from(await _timelineFragmentsBox.get(key) ?? []);
    1104          51 :         final i = eventIds.indexWhere((id) => id == eventId);
    1105          66 :         if (i != -1) {
    1106           6 :           await _timelineFragmentsBox.put(key, eventIds..removeAt(i));
    1107             :         }
    1108             :       }
    1109             : 
    1110             :       // Is there a transaction id? Then delete the event with this id.
    1111          66 :       if (!status.isError && !status.isSending && transactionId != null) {
    1112          18 :         await removeEvent(transactionId, eventUpdate.roomID);
    1113             :       }
    1114             :     }
    1115             : 
    1116          66 :     final stateKey = eventUpdate.content['state_key'];
    1117             :     // Store a common state event
    1118             :     if (stateKey != null &&
    1119             :         // Don't store events as state updates when paginating backwards.
    1120          62 :         (eventUpdate.type == EventUpdateType.timeline ||
    1121          62 :             eventUpdate.type == EventUpdateType.state ||
    1122          62 :             eventUpdate.type == EventUpdateType.inviteState)) {
    1123          93 :       if (eventUpdate.content['type'] == EventTypes.RoomMember) {
    1124          60 :         await _roomMembersBox.put(
    1125          30 :             TupleKey(
    1126          30 :               eventUpdate.roomID,
    1127          60 :               eventUpdate.content['state_key'],
    1128          30 :             ).toString(),
    1129          30 :             eventUpdate.content);
    1130             :       } else {
    1131          62 :         final type = eventUpdate.content['type'] as String;
    1132          62 :         final roomStateBox = client.importantStateEvents.contains(type)
    1133          31 :             ? _preloadRoomStateBox
    1134          30 :             : _nonPreloadRoomStateBox;
    1135          31 :         final key = TupleKey(
    1136          31 :           eventUpdate.roomID,
    1137             :           type,
    1138          31 :         ).toString();
    1139          93 :         final stateMap = copyMap(await roomStateBox.get(key) ?? {});
    1140             : 
    1141          62 :         stateMap[stateKey] = eventUpdate.content;
    1142          31 :         await roomStateBox.put(key, stateMap);
    1143             :       }
    1144             :     }
    1145             : 
    1146             :     // Store a room account data event
    1147          66 :     if (eventUpdate.type == EventUpdateType.accountData) {
    1148          60 :       await _roomAccountDataBox.put(
    1149          30 :         TupleKey(
    1150          30 :           eventUpdate.roomID,
    1151          60 :           eventUpdate.content['type'],
    1152          30 :         ).toString(),
    1153          30 :         eventUpdate.content,
    1154             :       );
    1155             :     }
    1156             :   }
    1157             : 
    1158          24 :   @override
    1159             :   Future<void> storeInboundGroupSession(
    1160             :       String roomId,
    1161             :       String sessionId,
    1162             :       String pickle,
    1163             :       String content,
    1164             :       String indexes,
    1165             :       String allowedAtIndex,
    1166             :       String senderKey,
    1167             :       String senderClaimedKey) async {
    1168          24 :     final json = StoredInboundGroupSession(
    1169             :       roomId: roomId,
    1170             :       sessionId: sessionId,
    1171             :       pickle: pickle,
    1172             :       content: content,
    1173             :       indexes: indexes,
    1174             :       allowedAtIndex: allowedAtIndex,
    1175             :       senderKey: senderKey,
    1176             :       senderClaimedKeys: senderClaimedKey,
    1177          24 :     ).toJson();
    1178          48 :     await _inboundGroupSessionsBox.put(
    1179             :       sessionId,
    1180             :       json,
    1181             :     );
    1182             :     // Mark this session as needing upload too
    1183          48 :     await _inboundGroupSessionsUploadQueueBox.put(sessionId, roomId);
    1184             :     return;
    1185             :   }
    1186             : 
    1187           6 :   @override
    1188             :   Future<void> storeOutboundGroupSession(
    1189             :       String roomId, String pickle, String deviceIds, int creationTime) async {
    1190          18 :     await _outboundGroupSessionsBox.put(roomId, <String, dynamic>{
    1191             :       'room_id': roomId,
    1192             :       'pickle': pickle,
    1193             :       'device_ids': deviceIds,
    1194             :       'creation_time': creationTime,
    1195             :     });
    1196             :     return;
    1197             :   }
    1198             : 
    1199          30 :   @override
    1200             :   Future<void> storePrevBatch(
    1201             :     String prevBatch,
    1202             :   ) async {
    1203          90 :     if ((await _clientBox.getAllKeys()).isEmpty) return;
    1204          60 :     await _clientBox.put('prev_batch', prevBatch);
    1205             :     return;
    1206             :   }
    1207             : 
    1208          31 :   @override
    1209             :   Future<void> storeRoomUpdate(String roomId, SyncRoomUpdate roomUpdate,
    1210             :       Event? lastEvent, Client client) async {
    1211             :     // Leave room if membership is leave
    1212          31 :     if (roomUpdate is LeftRoomUpdate) {
    1213          30 :       await forgetRoom(roomId);
    1214             :       return;
    1215             :     }
    1216          31 :     final membership = roomUpdate is LeftRoomUpdate
    1217             :         ? Membership.leave
    1218          31 :         : roomUpdate is InvitedRoomUpdate
    1219             :             ? Membership.invite
    1220             :             : Membership.join;
    1221             :     // Make sure room exists
    1222          62 :     final currentRawRoom = await _roomsBox.get(roomId);
    1223             :     if (currentRawRoom == null) {
    1224          62 :       await _roomsBox.put(
    1225             :           roomId,
    1226          31 :           roomUpdate is JoinedRoomUpdate
    1227          31 :               ? Room(
    1228             :                   client: client,
    1229             :                   id: roomId,
    1230             :                   membership: membership,
    1231             :                   highlightCount:
    1232          91 :                       roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
    1233             :                           0,
    1234             :                   notificationCount: roomUpdate
    1235          61 :                           .unreadNotifications?.notificationCount
    1236          30 :                           ?.toInt() ??
    1237             :                       0,
    1238          61 :                   prev_batch: roomUpdate.timeline?.prevBatch,
    1239          31 :                   summary: roomUpdate.summary,
    1240             :                   lastEvent: lastEvent,
    1241          31 :                 ).toJson()
    1242          30 :               : Room(
    1243             :                   client: client,
    1244             :                   id: roomId,
    1245             :                   membership: membership,
    1246             :                   lastEvent: lastEvent,
    1247          30 :                 ).toJson());
    1248          11 :     } else if (roomUpdate is JoinedRoomUpdate) {
    1249          22 :       final currentRoom = Room.fromJson(copyMap(currentRawRoom), client);
    1250          22 :       await _roomsBox.put(
    1251             :           roomId,
    1252          11 :           Room(
    1253             :             client: client,
    1254             :             id: roomId,
    1255             :             membership: membership,
    1256             :             highlightCount:
    1257          13 :                 roomUpdate.unreadNotifications?.highlightCount?.toInt() ??
    1258          11 :                     currentRoom.highlightCount,
    1259             :             notificationCount:
    1260          13 :                 roomUpdate.unreadNotifications?.notificationCount?.toInt() ??
    1261          11 :                     currentRoom.notificationCount,
    1262             :             prev_batch:
    1263          32 :                 roomUpdate.timeline?.prevBatch ?? currentRoom.prev_batch,
    1264          33 :             summary: RoomSummary.fromJson(currentRoom.summary.toJson()
    1265          34 :               ..addAll(roomUpdate.summary?.toJson() ?? {})),
    1266             :             lastEvent: lastEvent,
    1267          11 :           ).toJson());
    1268             :     }
    1269             :   }
    1270             : 
    1271          30 :   @override
    1272             :   Future<void> deleteTimelineForRoom(String roomId) =>
    1273         120 :       _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
    1274             : 
    1275           8 :   @override
    1276             :   Future<void> storeSSSSCache(
    1277             :       String type, String keyId, String ciphertext, String content) async {
    1278          16 :     await _ssssCacheBox.put(
    1279             :         type,
    1280           8 :         SSSSCache(
    1281             :           type: type,
    1282             :           keyId: keyId,
    1283             :           ciphertext: ciphertext,
    1284             :           content: content,
    1285           8 :         ).toJson());
    1286             :   }
    1287             : 
    1288          31 :   @override
    1289             :   Future<void> storeSyncFilterId(
    1290             :     String syncFilterId,
    1291             :   ) async {
    1292          62 :     await _clientBox.put('sync_filter_id', syncFilterId);
    1293             :   }
    1294             : 
    1295          31 :   @override
    1296             :   Future<void> storeUserCrossSigningKey(String userId, String publicKey,
    1297             :       String content, bool verified, bool blocked) async {
    1298          62 :     await _userCrossSigningKeysBox.put(
    1299          62 :       TupleKey(userId, publicKey).toString(),
    1300          31 :       {
    1301             :         'user_id': userId,
    1302             :         'public_key': publicKey,
    1303             :         'content': content,
    1304             :         'verified': verified,
    1305             :         'blocked': blocked,
    1306             :       },
    1307             :     );
    1308             :   }
    1309             : 
    1310          31 :   @override
    1311             :   Future<void> storeUserDeviceKey(String userId, String deviceId,
    1312             :       String content, bool verified, bool blocked, int lastActive) async {
    1313         155 :     await _userDeviceKeysBox.put(TupleKey(userId, deviceId).toString(), {
    1314             :       'user_id': userId,
    1315             :       'device_id': deviceId,
    1316             :       'content': content,
    1317             :       'verified': verified,
    1318             :       'blocked': blocked,
    1319             :       'last_active': lastActive,
    1320             :       'last_sent_message': '',
    1321             :     });
    1322             :     return;
    1323             :   }
    1324             : 
    1325          31 :   @override
    1326             :   Future<void> storeUserDeviceKeysInfo(String userId, bool outdated) async {
    1327          62 :     await _userDeviceKeysOutdatedBox.put(userId, outdated);
    1328             :     return;
    1329             :   }
    1330             : 
    1331          33 :   @override
    1332             :   Future<void> transaction(Future<void> Function() action) =>
    1333          66 :       _collection.transaction(action);
    1334             : 
    1335           2 :   @override
    1336             :   Future<void> updateClient(
    1337             :     String homeserverUrl,
    1338             :     String token,
    1339             :     DateTime? tokenExpiresAt,
    1340             :     String? refreshToken,
    1341             :     String userId,
    1342             :     String? deviceId,
    1343             :     String? deviceName,
    1344             :     String? prevBatch,
    1345             :     String? olmAccount,
    1346             :   ) async {
    1347           4 :     await transaction(() async {
    1348           4 :       await _clientBox.put('homeserver_url', homeserverUrl);
    1349           4 :       await _clientBox.put('token', token);
    1350             :       if (tokenExpiresAt == null) {
    1351           0 :         await _clientBox.delete('token_expires_at');
    1352             :       } else {
    1353           4 :         await _clientBox.put('token_expires_at',
    1354           4 :             tokenExpiresAt.millisecondsSinceEpoch.toString());
    1355             :       }
    1356             :       if (refreshToken == null) {
    1357           0 :         await _clientBox.delete('refresh_token');
    1358             :       } else {
    1359           4 :         await _clientBox.put('refresh_token', refreshToken);
    1360             :       }
    1361           4 :       await _clientBox.put('user_id', userId);
    1362             :       if (deviceId == null) {
    1363           0 :         await _clientBox.delete('device_id');
    1364             :       } else {
    1365           4 :         await _clientBox.put('device_id', deviceId);
    1366             :       }
    1367             :       if (deviceName == null) {
    1368           0 :         await _clientBox.delete('device_name');
    1369             :       } else {
    1370           4 :         await _clientBox.put('device_name', deviceName);
    1371             :       }
    1372             :       if (prevBatch == null) {
    1373           0 :         await _clientBox.delete('prev_batch');
    1374             :       } else {
    1375           4 :         await _clientBox.put('prev_batch', prevBatch);
    1376             :       }
    1377             :       if (olmAccount == null) {
    1378           0 :         await _clientBox.delete('olm_account');
    1379             :       } else {
    1380           4 :         await _clientBox.put('olm_account', olmAccount);
    1381             :       }
    1382             :     });
    1383             :     return;
    1384             :   }
    1385             : 
    1386          24 :   @override
    1387             :   Future<void> updateClientKeys(
    1388             :     String olmAccount,
    1389             :   ) async {
    1390          48 :     await _clientBox.put('olm_account', olmAccount);
    1391             :     return;
    1392             :   }
    1393             : 
    1394           2 :   @override
    1395             :   Future<void> updateInboundGroupSessionAllowedAtIndex(
    1396             :       String allowedAtIndex, String roomId, String sessionId) async {
    1397           4 :     final raw = await _inboundGroupSessionsBox.get(sessionId);
    1398             :     if (raw == null) {
    1399           0 :       Logs().w(
    1400             :           'Tried to update inbound group session as uploaded which wasnt found in the database!');
    1401             :       return;
    1402             :     }
    1403           2 :     raw['allowed_at_index'] = allowedAtIndex;
    1404           4 :     await _inboundGroupSessionsBox.put(sessionId, raw);
    1405             :     return;
    1406             :   }
    1407             : 
    1408           4 :   @override
    1409             :   Future<void> updateInboundGroupSessionIndexes(
    1410             :       String indexes, String roomId, String sessionId) async {
    1411           8 :     final raw = await _inboundGroupSessionsBox.get(sessionId);
    1412             :     if (raw == null) {
    1413           0 :       Logs().w(
    1414             :           'Tried to update inbound group session indexes of a session which was not found in the database!');
    1415             :       return;
    1416             :     }
    1417           4 :     final json = copyMap(raw);
    1418           4 :     json['indexes'] = indexes;
    1419           8 :     await _inboundGroupSessionsBox.put(sessionId, json);
    1420             :     return;
    1421             :   }
    1422             : 
    1423           2 :   @override
    1424             :   Future<List<StoredInboundGroupSession>> getAllInboundGroupSessions() async {
    1425           4 :     final rawSessions = await _inboundGroupSessionsBox.getAllValues();
    1426           2 :     return rawSessions.values
    1427           5 :         .map((raw) => StoredInboundGroupSession.fromJson(copyMap(raw)))
    1428           2 :         .toList();
    1429             :   }
    1430             : 
    1431          30 :   @override
    1432             :   Future<void> addSeenDeviceId(
    1433             :     String userId,
    1434             :     String deviceId,
    1435             :     String publicKeys,
    1436             :   ) =>
    1437         120 :       _seenDeviceIdsBox.put(TupleKey(userId, deviceId).toString(), publicKeys);
    1438             : 
    1439          30 :   @override
    1440             :   Future<void> addSeenPublicKey(
    1441             :     String publicKey,
    1442             :     String deviceId,
    1443             :   ) =>
    1444          60 :       _seenDeviceKeysBox.put(publicKey, deviceId);
    1445             : 
    1446          30 :   @override
    1447             :   Future<String?> deviceIdSeen(userId, deviceId) async {
    1448             :     final raw =
    1449         120 :         await _seenDeviceIdsBox.get(TupleKey(userId, deviceId).toString());
    1450             :     if (raw == null) return null;
    1451             :     return raw;
    1452             :   }
    1453             : 
    1454          30 :   @override
    1455             :   Future<String?> publicKeySeen(String publicKey) async {
    1456          60 :     final raw = await _seenDeviceKeysBox.get(publicKey);
    1457             :     if (raw == null) return null;
    1458             :     return raw;
    1459             :   }
    1460             : 
    1461           0 :   @override
    1462             :   Future<String> exportDump() async {
    1463           0 :     final dataMap = {
    1464           0 :       _clientBoxName: await _clientBox.getAllValues(),
    1465           0 :       _accountDataBoxName: await _accountDataBox.getAllValues(),
    1466           0 :       _roomsBoxName: await _roomsBox.getAllValues(),
    1467           0 :       _preloadRoomStateBoxName: await _preloadRoomStateBox.getAllValues(),
    1468           0 :       _nonPreloadRoomStateBoxName: await _nonPreloadRoomStateBox.getAllValues(),
    1469           0 :       _roomMembersBoxName: await _roomMembersBox.getAllValues(),
    1470           0 :       _toDeviceQueueBoxName: await _toDeviceQueueBox.getAllValues(),
    1471           0 :       _roomAccountDataBoxName: await _roomAccountDataBox.getAllValues(),
    1472             :       _inboundGroupSessionsBoxName:
    1473           0 :           await _inboundGroupSessionsBox.getAllValues(),
    1474             :       _inboundGroupSessionsUploadQueueBoxName:
    1475           0 :           await _inboundGroupSessionsUploadQueueBox.getAllValues(),
    1476             :       _outboundGroupSessionsBoxName:
    1477           0 :           await _outboundGroupSessionsBox.getAllValues(),
    1478           0 :       _olmSessionsBoxName: await _olmSessionsBox.getAllValues(),
    1479           0 :       _userDeviceKeysBoxName: await _userDeviceKeysBox.getAllValues(),
    1480             :       _userDeviceKeysOutdatedBoxName:
    1481           0 :           await _userDeviceKeysOutdatedBox.getAllValues(),
    1482             :       _userCrossSigningKeysBoxName:
    1483           0 :           await _userCrossSigningKeysBox.getAllValues(),
    1484           0 :       _ssssCacheBoxName: await _ssssCacheBox.getAllValues(),
    1485           0 :       _presencesBoxName: await _presencesBox.getAllValues(),
    1486           0 :       _timelineFragmentsBoxName: await _timelineFragmentsBox.getAllValues(),
    1487           0 :       _eventsBoxName: await _eventsBox.getAllValues(),
    1488           0 :       _seenDeviceIdsBoxName: await _seenDeviceIdsBox.getAllValues(),
    1489           0 :       _seenDeviceKeysBoxName: await _seenDeviceKeysBox.getAllValues(),
    1490             :     };
    1491           0 :     final json = jsonEncode(dataMap);
    1492           0 :     await clear();
    1493             :     return json;
    1494             :   }
    1495             : 
    1496           0 :   @override
    1497             :   Future<bool> importDump(String export) async {
    1498             :     try {
    1499           0 :       await clear();
    1500           0 :       await open();
    1501           0 :       final json = Map.from(jsonDecode(export)).cast<String, Map>();
    1502           0 :       for (final key in json[_clientBoxName]!.keys) {
    1503           0 :         await _clientBox.put(key, json[_clientBoxName]![key]);
    1504             :       }
    1505           0 :       for (final key in json[_accountDataBoxName]!.keys) {
    1506           0 :         await _accountDataBox.put(key, json[_accountDataBoxName]![key]);
    1507             :       }
    1508           0 :       for (final key in json[_roomsBoxName]!.keys) {
    1509           0 :         await _roomsBox.put(key, json[_roomsBoxName]![key]);
    1510             :       }
    1511           0 :       for (final key in json[_preloadRoomStateBoxName]!.keys) {
    1512           0 :         await _preloadRoomStateBox.put(
    1513           0 :             key, json[_preloadRoomStateBoxName]![key]);
    1514             :       }
    1515           0 :       for (final key in json[_nonPreloadRoomStateBoxName]!.keys) {
    1516           0 :         await _nonPreloadRoomStateBox.put(
    1517           0 :             key, json[_nonPreloadRoomStateBoxName]![key]);
    1518             :       }
    1519           0 :       for (final key in json[_roomMembersBoxName]!.keys) {
    1520           0 :         await _roomMembersBox.put(key, json[_roomMembersBoxName]![key]);
    1521             :       }
    1522           0 :       for (final key in json[_toDeviceQueueBoxName]!.keys) {
    1523           0 :         await _toDeviceQueueBox.put(key, json[_toDeviceQueueBoxName]![key]);
    1524             :       }
    1525           0 :       for (final key in json[_roomAccountDataBoxName]!.keys) {
    1526           0 :         await _roomAccountDataBox.put(key, json[_roomAccountDataBoxName]![key]);
    1527             :       }
    1528           0 :       for (final key in json[_inboundGroupSessionsBoxName]!.keys) {
    1529           0 :         await _inboundGroupSessionsBox.put(
    1530           0 :             key, json[_inboundGroupSessionsBoxName]![key]);
    1531             :       }
    1532           0 :       for (final key in json[_inboundGroupSessionsUploadQueueBoxName]!.keys) {
    1533           0 :         await _inboundGroupSessionsUploadQueueBox.put(
    1534           0 :             key, json[_inboundGroupSessionsUploadQueueBoxName]![key]);
    1535             :       }
    1536           0 :       for (final key in json[_outboundGroupSessionsBoxName]!.keys) {
    1537           0 :         await _outboundGroupSessionsBox.put(
    1538           0 :             key, json[_outboundGroupSessionsBoxName]![key]);
    1539             :       }
    1540           0 :       for (final key in json[_olmSessionsBoxName]!.keys) {
    1541           0 :         await _olmSessionsBox.put(key, json[_olmSessionsBoxName]![key]);
    1542             :       }
    1543           0 :       for (final key in json[_userDeviceKeysBoxName]!.keys) {
    1544           0 :         await _userDeviceKeysBox.put(key, json[_userDeviceKeysBoxName]![key]);
    1545             :       }
    1546           0 :       for (final key in json[_userDeviceKeysOutdatedBoxName]!.keys) {
    1547           0 :         await _userDeviceKeysOutdatedBox.put(
    1548           0 :             key, json[_userDeviceKeysOutdatedBoxName]![key]);
    1549             :       }
    1550           0 :       for (final key in json[_userCrossSigningKeysBoxName]!.keys) {
    1551           0 :         await _userCrossSigningKeysBox.put(
    1552           0 :             key, json[_userCrossSigningKeysBoxName]![key]);
    1553             :       }
    1554           0 :       for (final key in json[_ssssCacheBoxName]!.keys) {
    1555           0 :         await _ssssCacheBox.put(key, json[_ssssCacheBoxName]![key]);
    1556             :       }
    1557           0 :       for (final key in json[_presencesBoxName]!.keys) {
    1558           0 :         await _presencesBox.put(key, json[_presencesBoxName]![key]);
    1559             :       }
    1560           0 :       for (final key in json[_timelineFragmentsBoxName]!.keys) {
    1561           0 :         await _timelineFragmentsBox.put(
    1562           0 :             key, json[_timelineFragmentsBoxName]![key]);
    1563             :       }
    1564           0 :       for (final key in json[_seenDeviceIdsBoxName]!.keys) {
    1565           0 :         await _seenDeviceIdsBox.put(key, json[_seenDeviceIdsBoxName]![key]);
    1566             :       }
    1567           0 :       for (final key in json[_seenDeviceKeysBoxName]!.keys) {
    1568           0 :         await _seenDeviceKeysBox.put(key, json[_seenDeviceKeysBoxName]![key]);
    1569             :       }
    1570             :       return true;
    1571             :     } catch (e, s) {
    1572           0 :       Logs().e('Database import error: ', e, s);
    1573             :       return false;
    1574             :     }
    1575             :   }
    1576             : 
    1577           1 :   @override
    1578             :   Future<List<String>> getEventIdList(
    1579             :     Room room, {
    1580             :     int start = 0,
    1581             :     bool includeSending = false,
    1582             :     int? limit,
    1583             :   }) =>
    1584           2 :       runBenchmarked<List<String>>('Get event id list', () async {
    1585             :         // Get the synced event IDs from the store
    1586           3 :         final timelineKey = TupleKey(room.id, '').toString();
    1587           1 :         final timelineEventIds = List<String>.from(
    1588           2 :             (await _timelineFragmentsBox.get(timelineKey)) ?? []);
    1589             : 
    1590             :         // Get the local stored SENDING events from the store
    1591             :         late final List<String> sendingEventIds;
    1592             :         if (!includeSending) {
    1593           1 :           sendingEventIds = [];
    1594             :         } else {
    1595           0 :           final sendingTimelineKey = TupleKey(room.id, 'SENDING').toString();
    1596           0 :           sendingEventIds = List<String>.from(
    1597           0 :               (await _timelineFragmentsBox.get(sendingTimelineKey)) ?? []);
    1598             :         }
    1599             : 
    1600             :         // Combine those two lists while respecting the start and limit parameters.
    1601             :         // Create a new list object instead of concatonating list to prevent
    1602             :         // random type errors.
    1603           1 :         final eventIds = [
    1604             :           ...sendingEventIds,
    1605           1 :           ...timelineEventIds,
    1606             :         ];
    1607           0 :         if (limit != null && eventIds.length > limit) {
    1608           0 :           eventIds.removeRange(limit, eventIds.length);
    1609             :         }
    1610             : 
    1611             :         return eventIds;
    1612             :       });
    1613             : 
    1614          31 :   @override
    1615             :   Future<void> storePresence(String userId, CachedPresence presence) =>
    1616          93 :       _presencesBox.put(userId, presence.toJson());
    1617             : 
    1618           1 :   @override
    1619             :   Future<CachedPresence?> getPresence(String userId) async {
    1620           2 :     final rawPresence = await _presencesBox.get(userId);
    1621             :     if (rawPresence == null) return null;
    1622             : 
    1623           2 :     return CachedPresence.fromJson(copyMap(rawPresence));
    1624             :   }
    1625             : 
    1626           1 :   @override
    1627             :   Future<void> storeWellKnown(DiscoveryInformation? discoveryInformation) {
    1628             :     if (discoveryInformation == null) {
    1629           2 :       return _clientBox.delete('discovery_information');
    1630             :     }
    1631           2 :     return _clientBox.put(
    1632             :       'discovery_information',
    1633           2 :       jsonEncode(discoveryInformation.toJson()),
    1634             :     );
    1635             :   }
    1636             : 
    1637          30 :   @override
    1638             :   Future<DiscoveryInformation?> getWellKnown() async {
    1639             :     final rawDiscoveryInformation =
    1640          60 :         await _clientBox.get('discovery_information');
    1641             :     if (rawDiscoveryInformation == null) return null;
    1642           2 :     return DiscoveryInformation.fromJson(jsonDecode(rawDiscoveryInformation));
    1643             :   }
    1644             : 
    1645           9 :   @override
    1646             :   Future<void> delete() async {
    1647             :     // database?.path is null on web
    1648          18 :     await _collection.deleteDatabase(
    1649          18 :       database?.path ?? name,
    1650           9 :       sqfliteFactory ?? idbFactory,
    1651             :     );
    1652             :   }
    1653             : 
    1654          31 :   @override
    1655             :   Future<void> markUserProfileAsOutdated(userId) async {
    1656          31 :     final profile = await getUserProfile(userId);
    1657             :     if (profile == null) return;
    1658           4 :     await _userProfilesBox.put(
    1659             :       userId,
    1660           2 :       CachedProfileInformation.fromProfile(
    1661             :         profile as ProfileInformation,
    1662             :         outdated: true,
    1663           2 :         updated: profile.updated,
    1664           2 :       ).toJson(),
    1665             :     );
    1666             :   }
    1667             : 
    1668          31 :   @override
    1669             :   Future<CachedProfileInformation?> getUserProfile(String userId) =>
    1670         124 :       _userProfilesBox.get(userId).then((json) => json == null
    1671             :           ? null
    1672           4 :           : CachedProfileInformation.fromJson(copyMap(json)));
    1673             : 
    1674           4 :   @override
    1675             :   Future<void> storeUserProfile(
    1676             :           String userId, CachedProfileInformation profile) =>
    1677           8 :       _userProfilesBox.put(
    1678             :         userId,
    1679           4 :         profile.toJson(),
    1680             :       );
    1681             : }

Generated by: LCOV version 1.14