Line data Source code
1 : import 'dart:async'; 2 : import 'package:flutter/foundation.dart'; 3 : import 'message.dart'; 4 : 5 : // we only count up to 100 unread messages, if more than that we can't accurately resync message cache, just reset 6 : // https://git.openprivacy.ca/cwtch.im/libcwtch-go/src/branch/trunk/utils/eventHandler.go#L210 7 : const MaxUnreadBeforeCacheReset = 100; 8 : 9 : class MessageInfo { 10 : late MessageMetadata metadata; 11 : late String wrapper; 12 : 13 0 : MessageInfo(this.metadata, this.wrapper); 14 : 15 0 : int size() { 16 0 : var wrapperSize = wrapper.length * 2; 17 : return wrapperSize; 18 : } 19 : } 20 : 21 : class LocalIndexMessage { 22 : late bool cacheOnly; 23 : late bool isLoading; 24 : late Future<void> loaded; 25 : late Completer<void> loader; 26 : 27 : late int? messageId; 28 : 29 0 : LocalIndexMessage(int? messageId, {cacheOnly = false, isLoading = false}) { 30 0 : this.messageId = messageId; 31 0 : this.cacheOnly = cacheOnly; 32 0 : this.isLoading = isLoading; 33 0 : loader = Completer<void>(); 34 0 : loaded = loader.future; 35 : if (!isLoading) { 36 0 : loader.complete(); // complete this 37 : } 38 : } 39 : 40 0 : void finishLoad(int messageId) { 41 0 : this.messageId = messageId; 42 0 : if (!loader.isCompleted) { 43 0 : isLoading = false; 44 0 : loader.complete(true); 45 : } 46 : } 47 : 48 0 : void failLoad() { 49 0 : this.messageId = null; 50 0 : if (!loader.isCompleted) { 51 0 : isLoading = false; 52 0 : loader.complete(true); 53 : } 54 : } 55 : 56 0 : Future<void> waitForLoad() { 57 0 : return loaded; 58 : } 59 : 60 0 : Future<int?> get() async { 61 0 : if (isLoading) { 62 0 : await waitForLoad(); 63 : } 64 0 : return messageId; 65 : } 66 : } 67 : 68 : // Message cache stores messages for use by the UI and uses MessageHandler and associated ByX loaders 69 : // the cache stores messages in a cache indexed by their storage Id, and has two secondary indexes into it, content hash, and local index 70 : // Index is the primary way to access the cache as it is a sequential ordered access and is used by the message pane 71 : // contentHash is used for fetching replies 72 : // by Id is used when composing a reply 73 : // cacheByIndex supports additional features than just a direct index into the cache (byID) 74 : // it allows locking of ranges in order to support bulk sequential loading (see ByIndex in message.dart) 75 : // cacheByIndex allows allows inserting temporarily non storage backed messages so that Send Message can be respected instantly and then updated upon insertion into backend 76 : // the message cache needs storageMessageCount maintained by the system so it can inform bulk loading when it's reaching the end of fetchable messages 77 : class MessageCache extends ChangeNotifier { 78 : // cache of MessageId to Message 79 : late Map<int, MessageInfo> cache; 80 : 81 : // local index to MessageId 82 : late List<LocalIndexMessage> cacheByIndex; 83 : // 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 84 : int _indexUnsynced = 0; 85 : 86 : // map of content hash to MessageId 87 : late Map<String, int> cacheByHash; 88 : 89 : late int _storageMessageCount; 90 : 91 0 : MessageCache(int storageMessageCount) { 92 0 : cache = {}; 93 0 : cacheByIndex = List.empty(growable: true); 94 0 : cacheByHash = {}; 95 0 : this._storageMessageCount = storageMessageCount; 96 : } 97 : 98 0 : int get storageMessageCount => _storageMessageCount; 99 0 : set storageMessageCount(int newval) { 100 0 : this._storageMessageCount = newval; 101 : } 102 : 103 : // On android reconnect, if backend supplied message count > UI message count, add the difference to the front of the index 104 0 : void addFrontIndexGap(int count) { 105 0 : this._indexUnsynced = count; 106 : } 107 : 108 0 : int get indexUnsynced => _indexUnsynced; 109 : 110 0 : MessageInfo? getById(int id) => cache[id]; 111 : 112 0 : Future<MessageInfo?> getByIndex(int index) async { 113 0 : if (index >= cacheByIndex.length) { 114 : return null; 115 : } 116 0 : var id = await cacheByIndex[index].get(); 117 : if (id == null) { 118 0 : return Future<MessageInfo?>.value(null); 119 : } 120 0 : return cache[id]; 121 : } 122 : 123 0 : int findIndex(int id) { 124 0 : return cacheByIndex.indexWhere((element) => element.messageId == id); 125 : } 126 : 127 0 : MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]]; 128 : 129 0 : void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash) { 130 0 : this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto, contenthash), data); 131 0 : this.cache[messageID]?.metadata.lastChecked = DateTime.now(); // Don't check straight away... 132 0 : this.cacheByIndex.insert(0, LocalIndexMessage(messageID)); 133 0 : if (contenthash != "") { 134 0 : this.cacheByHash[contenthash] = messageID; 135 : } 136 : } 137 : 138 : // inserts place holder values into the index cache that will block on .get() until .finishLoad() is called on them with message contents 139 : // or .failLoad() is called on them to mark them malformed 140 : // 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 141 0 : void lockIndexes(int start, int end) { 142 0 : for (var i = start; i < end; i++) { 143 0 : this.cacheByIndex.insert(i, LocalIndexMessage(null, isLoading: true)); 144 : // 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 145 : // there for we can decrement the count as this will be one of them 146 0 : if (this._indexUnsynced > 0) { 147 0 : this._indexUnsynced--; 148 : } 149 : } 150 : } 151 : 152 0 : void malformIndexes(int start, int end) { 153 0 : for (var i = start; i < end; i++) { 154 0 : this.cacheByIndex[i].failLoad(); 155 : } 156 : } 157 : 158 0 : void addIndexed(MessageInfo messageInfo, int index) { 159 0 : this.cache[messageInfo.metadata.messageID] = messageInfo; 160 0 : if (index < this.cacheByIndex.length) { 161 0 : this.cacheByIndex[index].finishLoad(messageInfo.metadata.messageID); 162 : } else { 163 0 : this.cacheByIndex.insert(index, LocalIndexMessage(messageInfo.metadata.messageID)); 164 : } 165 0 : this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID; 166 : } 167 : 168 0 : void addUnindexed(MessageInfo messageInfo) { 169 0 : this.cache[messageInfo.metadata.messageID] = messageInfo; 170 0 : if (messageInfo.metadata.contenthash != "") { 171 0 : this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID; 172 : } 173 : } 174 : 175 0 : void ackCache(int messageID) { 176 0 : cache[messageID]?.metadata.ackd = true; 177 0 : notifyListeners(); 178 : } 179 : 180 0 : void errCache(int messageID) { 181 0 : cache[messageID]?.metadata.error = true; 182 0 : notifyListeners(); 183 : } 184 : 185 0 : void notifyUpdate(int messageID) { 186 0 : notifyListeners(); 187 : } 188 : 189 0 : int size() { 190 : // very naive cache size, assuming MessageInfo are fairly large on average 191 : // and everything else is small in comparison 192 0 : int cacheSize = cache.entries.map((e) => e.value.size()).fold(0, (previousValue, element) => previousValue + element); 193 0 : return cacheSize + cacheByHash.length * 64 + cacheByIndex.length * 16; 194 : } 195 : 196 0 : void updateTranslationEvent(int messageID, String translation) { 197 0 : cache[messageID]?.metadata.updateTranslationEvent(translation); 198 0 : notifyListeners(); 199 : } 200 : }