import 'dart:async'; import 'package:flutter/foundation.dart'; import 'message.dart'; // we only count up to 100 unread messages, if more than that we can't accurately resync message cache, just reset // https://git.openprivacy.ca/cwtch.im/libcwtch-go/src/branch/trunk/utils/eventHandler.go#L210 const MaxUnreadBeforeCacheReset = 100; class MessageInfo { late MessageMetadata metadata; late String wrapper; MessageInfo(this.metadata, this.wrapper); int size() { var wrapperSize = wrapper.length * 2; return wrapperSize; } } class LocalIndexMessage { late bool cacheOnly; late bool isLoading; late Future loaded; late Completer loader; late int? messageId; LocalIndexMessage(int? messageId, {cacheOnly = false, isLoading = false}) { this.messageId = messageId; this.cacheOnly = cacheOnly; this.isLoading = isLoading; loader = Completer(); loaded = loader.future; if (!isLoading) { loader.complete(); // complete this } } void finishLoad(int messageId) { this.messageId = messageId; if (!loader.isCompleted) { isLoading = false; loader.complete(true); } } void failLoad() { this.messageId = null; if (!loader.isCompleted) { isLoading = false; loader.complete(true); } } Future waitForLoad() { return loaded; } Future get() async { if (isLoading) { await waitForLoad(); } return messageId; } } // Message cache stores messages for use by the UI and uses MessageHandler and associated ByX loaders // the cache stores messages in a cache indexed by their storage Id, and has two secondary indexes into it, content hash, and local index // Index is the primary way to access the cache as it is a sequential ordered access and is used by the message pane // contentHash is used for fetching replies // by Id is used when composing a reply // cacheByIndex supports additional features than just a direct index into the cache (byID) // it allows locking of ranges in order to support bulk sequential loading (see ByIndex in message.dart) // cacheByIndex allows allows inserting temporarily non storage backed messages so that Send Message can be respected instantly and then updated upon insertion into backend // the message cache needs storageMessageCount maintained by the system so it can inform bulk loading when it's reaching the end of fetchable messages class MessageCache extends ChangeNotifier { // cache of MessageId to Message late Map cache; // local index to MessageId late List cacheByIndex; // index unsynced is used on android on reconnect to tell us new messages are in the backend that should be at the front of the index cache int _indexUnsynced = 0; // map of content hash to MessageId late Map cacheByHash; late int _storageMessageCount; MessageCache(int storageMessageCount) { cache = {}; cacheByIndex = List.empty(growable: true); cacheByHash = {}; this._storageMessageCount = storageMessageCount; } int get storageMessageCount => _storageMessageCount; set storageMessageCount(int newval) { this._storageMessageCount = newval; } // On android reconnect, if backend supplied message count > UI message count, add the difference to the front of the index void addFrontIndexGap(int count) { this._indexUnsynced = count; } int get indexUnsynced => _indexUnsynced; MessageInfo? getById(int id) => cache[id]; Future getByIndex(int index) async { if (index >= cacheByIndex.length) { return null; } var id = await cacheByIndex[index].get(); if (id == null) { return Future.value(null); } return cache[id]; } int findIndex(int id) { return cacheByIndex.indexWhere((element) => element.messageId == id); } MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]]; void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash) { this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto, contenthash), data); this.cache[messageID]?.metadata.lastChecked = DateTime.now(); // Don't check straight away... this.cacheByIndex.insert(0, LocalIndexMessage(messageID)); if (contenthash != "") { this.cacheByHash[contenthash] = messageID; } } // inserts place holder values into the index cache that will block on .get() until .finishLoad() is called on them with message contents // or .failLoad() is called on them to mark them malformed // this prevents successive ui message build requests from triggering multiple GetMesssage requests to the backend, as the first one locks a block of messages and the rest wait on that void lockIndexes(int start, int end) { for (var i = start; i < end; i++) { this.cacheByIndex.insert(i, LocalIndexMessage(null, isLoading: true)); // if there are unsynced messages on the index cache it means there are messages at the front, and by the logic in message/ByIndex/get() we will be loading those // there for we can decrement the count as this will be one of them if (this._indexUnsynced > 0) { this._indexUnsynced--; } } } void malformIndexes(int start, int end) { for (var i = start; i < end; i++) { this.cacheByIndex[i].failLoad(); } } void addIndexed(MessageInfo messageInfo, int index) { this.cache[messageInfo.metadata.messageID] = messageInfo; if (index < this.cacheByIndex.length) { this.cacheByIndex[index].finishLoad(messageInfo.metadata.messageID); } else { this.cacheByIndex.insert(index, LocalIndexMessage(messageInfo.metadata.messageID)); } this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID; } void addUnindexed(MessageInfo messageInfo) { this.cache[messageInfo.metadata.messageID] = messageInfo; if (messageInfo.metadata.contenthash != "") { this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID; } } void ackCache(int messageID) { cache[messageID]?.metadata.ackd = true; notifyListeners(); } void errCache(int messageID) { cache[messageID]?.metadata.error = true; notifyListeners(); } void notifyUpdate(int messageID) { notifyListeners(); } int size() { // very naive cache size, assuming MessageInfo are fairly large on average // and everything else is small in comparison int cacheSize = cache.entries.map((e) => e.value.size()).fold(0, (previousValue, element) => previousValue + element); return cacheSize + cacheByHash.length * 64 + cacheByIndex.length * 16; } void updateTranslationEvent(int messageID, String translation) { cache[messageID]?.metadata.updateTranslationEvent(translation); notifyListeners(); } }