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 0 : int get uiLoadedMessages => this.cacheByIndex.length;
104 :
105 : // On android reconnect, if backend supplied message count > UI message count, add the difference to the front of the index
106 0 : void addFrontIndexGap(int count) {
107 0 : this._indexUnsynced = count;
108 : }
109 :
110 0 : int get indexUnsynced => _indexUnsynced;
111 :
112 0 : MessageInfo? getById(int id) => cache[id];
113 :
114 0 : Future<MessageInfo?> getByIndex(int index) async {
115 0 : if (index >= cacheByIndex.length) {
116 : return null;
117 : }
118 0 : var id = await cacheByIndex[index].get();
119 : if (id == null) {
120 0 : return Future<MessageInfo?>.value(null);
121 : }
122 0 : return cache[id];
123 : }
124 :
125 0 : int findIndex(int id) {
126 0 : return cacheByIndex.indexWhere((element) => element.messageId == id);
127 : }
128 :
129 0 : MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]];
130 :
131 0 : void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash, String signature) {
132 0 : this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, signature, {}, false, false, isAuto, contenthash), data);
133 0 : this.cache[messageID]?.metadata.lastChecked = DateTime.now(); // Don't check straight away...
134 0 : this.cacheByIndex.insert(0, LocalIndexMessage(messageID));
135 0 : if (contenthash != "") {
136 0 : this.cacheByHash[contenthash] = messageID;
137 : }
138 0 : notifyListeners();
139 : }
140 :
141 : // inserts place holder values into the index cache that will block on .get() until .finishLoad() is called on them with message contents
142 : // or .failLoad() is called on them to mark them malformed
143 : // 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
144 0 : void lockIndexes(int start, int end) {
145 0 : for (var i = start; i < end; i++) {
146 0 : this.cacheByIndex.insert(i, LocalIndexMessage(null, isLoading: true));
147 : // 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
148 : // there for we can decrement the count as this will be one of them
149 0 : if (this._indexUnsynced > 0) {
150 0 : this._indexUnsynced--;
151 : }
152 : }
153 : }
154 :
155 0 : void malformIndexes(int start, int end) {
156 0 : for (var i = start; i < end; i++) {
157 0 : this.cacheByIndex[i].failLoad();
158 : }
159 : }
160 :
161 0 : void addIndexed(MessageInfo messageInfo, int index) {
162 0 : this.cache[messageInfo.metadata.messageID] = messageInfo;
163 0 : if (index < this.cacheByIndex.length) {
164 0 : this.cacheByIndex[index].finishLoad(messageInfo.metadata.messageID);
165 : } else {
166 0 : this.cacheByIndex.insert(index, LocalIndexMessage(messageInfo.metadata.messageID));
167 : }
168 0 : this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
169 : }
170 :
171 0 : void addUnindexed(MessageInfo messageInfo) {
172 0 : this.cache[messageInfo.metadata.messageID] = messageInfo;
173 0 : if (messageInfo.metadata.contenthash != "") {
174 0 : this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
175 : }
176 : }
177 :
178 0 : void ackCache(int messageID) {
179 0 : cache[messageID]?.metadata.ackd = true;
180 0 : notifyListeners();
181 : }
182 :
183 0 : void errCache(int messageID) {
184 0 : cache[messageID]?.metadata.error = true;
185 0 : notifyListeners();
186 : }
187 :
188 0 : void update(int messageID, String message, String? signature, dynamic attributes) {
189 0 : cache[messageID]?.wrapper = message;
190 : if (signature != null) {
191 0 : cache[messageID]?.metadata.signature = signature;
192 : }
193 0 : cache[messageID]?.metadata.attributes = attributes;
194 0 : notifyListeners();
195 : }
196 :
197 0 : void notifyUpdate(int messageID) {
198 0 : notifyListeners();
199 : }
200 :
201 0 : int size() {
202 : // very naive cache size, assuming MessageInfo are fairly large on average
203 : // and everything else is small in comparison
204 0 : int cacheSize = cache.entries.map((e) => e.value.size()).fold(0, (previousValue, element) => previousValue + element);
205 0 : return cacheSize + cacheByHash.length * 64 + cacheByIndex.length * 16;
206 : }
207 :
208 0 : void updateTranslationEvent(int messageID, String translation) {
209 0 : cache[messageID]?.metadata.updateTranslationEvent(translation);
210 0 : notifyListeners();
211 : }
212 : }
|