You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
6.7 KiB
195 lines
6.7 KiB
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<void> loaded;
|
|
late Completer<void> loader;
|
|
|
|
late int? messageId;
|
|
|
|
LocalIndexMessage(int? messageId, {cacheOnly = false, isLoading = false}) {
|
|
this.messageId = messageId;
|
|
this.cacheOnly = cacheOnly;
|
|
this.isLoading = isLoading;
|
|
if (isLoading) {
|
|
loader = Completer<void>();
|
|
loaded = loader.future;
|
|
}
|
|
}
|
|
|
|
void finishLoad(int messageId) {
|
|
this.messageId = messageId;
|
|
isLoading = false;
|
|
loader.complete(true);
|
|
}
|
|
|
|
void failLoad() {
|
|
this.messageId = null;
|
|
isLoading = false;
|
|
loader.complete(true);
|
|
}
|
|
|
|
Future<void> waitForLoad() {
|
|
return loaded;
|
|
}
|
|
|
|
Future<int?> 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<int, MessageInfo> cache;
|
|
|
|
// local index to MessageId
|
|
late List<LocalIndexMessage> 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<String, int> 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 differnce to the front of the index
|
|
void addFrontIndexGap(int count) {
|
|
this._indexUnsynced = count;
|
|
}
|
|
|
|
int get indexUnsynced => _indexUnsynced;
|
|
|
|
MessageInfo? getById(int id) => cache[id];
|
|
|
|
Future<MessageInfo?> getByIndex(int index) async {
|
|
if (index >= cacheByIndex.length) {
|
|
return null;
|
|
}
|
|
var id = await cacheByIndex[index].get();
|
|
if (id == null) {
|
|
return Future<MessageInfo?>.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.cacheByIndex.insert(0, LocalIndexMessage(messageID));
|
|
if (contenthash != null && 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();
|
|
}
|
|
}
|