message caching #326
|
@ -1 +1 @@
|
||||||
2022-01-19-16-16-v1.5.4-11-g84d451f
|
2022-01-20-12-53-v1.5.4-14-g6865ec1
|
|
@ -1 +1 @@
|
||||||
2022-01-19-21-15-v1.5.4-11-g84d451f
|
2022-01-20-17-53-v1.5.4-14-g6865ec1
|
|
@ -143,25 +143,20 @@ class CwtchNotifier {
|
||||||
var senderHandle = data['RemotePeer'];
|
var senderHandle = data['RemotePeer'];
|
||||||
var senderImage = data['Picture'];
|
var senderImage = data['Picture'];
|
||||||
var isAuto = data['Auto'] == "true";
|
var isAuto = data['Auto'] == "true";
|
||||||
|
String? contenthash = data['ContentHash'];
|
||||||
|
var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier;
|
||||||
|
|
||||||
// We might not have received a contact created for this contact yet...
|
profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage(
|
||||||
// In that case the **next** event we receive will actually update these values...
|
identifier,
|
||||||
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier) != null) {
|
messageID,
|
||||||
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
|
timestamp,
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
|
senderHandle,
|
||||||
} else {
|
senderImage,
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
|
isAuto,
|
||||||
}
|
data["Data"],
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now());
|
contenthash,
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data["Data"]);
|
selectedConversation,
|
||||||
|
);
|
||||||
// We only ever see messages from authenticated peers.
|
|
||||||
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
|
|
||||||
// end during syncing.
|
|
||||||
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) {
|
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "PeerAcknowledgement":
|
case "PeerAcknowledgement":
|
||||||
|
@ -200,18 +195,11 @@ class CwtchNotifier {
|
||||||
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
|
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
|
||||||
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages;
|
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages;
|
||||||
var isAuto = data['Auto'] == "true";
|
var isAuto = data['Auto'] == "true";
|
||||||
|
String? contenthash = data['ContentHash'];
|
||||||
|
var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier;
|
||||||
|
|
||||||
// Only bother to do anything if we know about the group and the provided index is greater than our current total...
|
// Only bother to do anything if we know about the group and the provided index is greater than our current total...
|
||||||
if (currentTotal != null && idx >= currentTotal) {
|
if (currentTotal != null && idx >= currentTotal) {
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"]);
|
|
||||||
|
|
||||||
//if not currently open
|
|
||||||
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
|
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
|
|
||||||
} else {
|
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: There are 2 timestamps associated with a new group message - time sent and time received.
|
// TODO: There are 2 timestamps associated with a new group message - time sent and time received.
|
||||||
// Sent refers to the time a profile alleges they sent a message
|
// Sent refers to the time a profile alleges they sent a message
|
||||||
// Received refers to the time we actually saw the message from the server
|
// Received refers to the time we actually saw the message from the server
|
||||||
|
@ -222,7 +210,8 @@ class CwtchNotifier {
|
||||||
// For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
|
// For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
|
||||||
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
|
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
|
||||||
// and `local now`.
|
// and `local now`.
|
||||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, timestampSent.toLocal());
|
profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedConversation);
|
||||||
|
|
||||||
notificationManager.notify("New Message From Group!");
|
notificationManager.notify("New Message From Group!");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,9 +19,9 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
late int _totalMessages = 0;
|
late int _totalMessages = 0;
|
||||||
late DateTime _lastMessageTime;
|
late DateTime _lastMessageTime;
|
||||||
late Map<String, GlobalKey<MessageRowState>> keys;
|
late Map<String, GlobalKey<MessageRowState>> keys;
|
||||||
late List<MessageCache?> messageCache;
|
|
||||||
int _newMarker = 0;
|
int _newMarker = 0;
|
||||||
DateTime _newMarkerClearAt = DateTime.now();
|
DateTime _newMarkerClearAt = DateTime.now();
|
||||||
|
late MessageCache messageCache;
|
||||||
|
|
||||||
// todo: a nicer way to model contacts, groups and other "entities"
|
// todo: a nicer way to model contacts, groups and other "entities"
|
||||||
late bool _isGroup;
|
late bool _isGroup;
|
||||||
|
@ -55,7 +55,7 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
|
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
|
||||||
this._server = server;
|
this._server = server;
|
||||||
this._archived = archived;
|
this._archived = archived;
|
||||||
this.messageCache = List.empty(growable: true);
|
this.messageCache = new MessageCache();
|
||||||
keys = Map<String, GlobalKey<MessageRowState>>();
|
keys = Map<String, GlobalKey<MessageRowState>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
String get savePeerHistory => this._savePeerHistory;
|
String get savePeerHistory => this._savePeerHistory;
|
||||||
|
|
||||||
String? get acnCircuit => this._acnCircuit;
|
String? get acnCircuit => this._acnCircuit;
|
||||||
|
|
||||||
set acnCircuit(String? acnCircuit) {
|
set acnCircuit(String? acnCircuit) {
|
||||||
this._acnCircuit = acnCircuit;
|
this._acnCircuit = acnCircuit;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -90,6 +91,7 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isGroup => this._isGroup;
|
bool get isGroup => this._isGroup;
|
||||||
|
|
||||||
set isGroup(bool newVal) {
|
set isGroup(bool newVal) {
|
||||||
this._isGroup = newVal;
|
this._isGroup = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -110,12 +112,14 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
String get status => this._status;
|
String get status => this._status;
|
||||||
|
|
||||||
set status(String newVal) {
|
set status(String newVal) {
|
||||||
this._status = newVal;
|
this._status = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
int get unreadMessages => this._unreadMessages;
|
int get unreadMessages => this._unreadMessages;
|
||||||
|
|
||||||
set unreadMessages(int newVal) {
|
set unreadMessages(int newVal) {
|
||||||
// don't reset newMarker position when unreadMessages is being cleared
|
// don't reset newMarker position when unreadMessages is being cleared
|
||||||
if (newVal > 0) {
|
if (newVal > 0) {
|
||||||
|
@ -149,18 +153,21 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
int get totalMessages => this._totalMessages;
|
int get totalMessages => this._totalMessages;
|
||||||
|
|
||||||
set totalMessages(int newVal) {
|
set totalMessages(int newVal) {
|
||||||
this._totalMessages = newVal;
|
this._totalMessages = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
String get imagePath => this._imagePath;
|
String get imagePath => this._imagePath;
|
||||||
|
|
||||||
set imagePath(String newVal) {
|
set imagePath(String newVal) {
|
||||||
this._imagePath = newVal;
|
this._imagePath = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime get lastMessageTime => this._lastMessageTime;
|
DateTime get lastMessageTime => this._lastMessageTime;
|
||||||
|
|
||||||
set lastMessageTime(DateTime newVal) {
|
set lastMessageTime(DateTime newVal) {
|
||||||
this._lastMessageTime = newVal;
|
this._lastMessageTime = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -197,18 +204,27 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) {
|
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) {
|
||||||
dan marked this conversation as resolved
|
|||||||
this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data));
|
if (!selectedConversation) {
|
||||||
this.totalMessages += 1;
|
unreadMessages++;
|
||||||
}
|
} else {
|
||||||
|
newMarker++;
|
||||||
|
}
|
||||||
|
|
||||||
void bumpMessageCache() {
|
this.messageCache.addNew(profileOnion, identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash);
|
||||||
this.messageCache.insert(0, null);
|
|
||||||
this.totalMessages += 1;
|
this.totalMessages += 1;
|
||||||
|
|
||||||
|
// We only ever see messages from authenticated peers.
|
||||||
|
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
|
||||||
|
// end during syncing.
|
||||||
|
if (isOnline() == false) {
|
||||||
|
status = "Authenticated";
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ackCache(int messageID) {
|
void ackCache(int messageID) {
|
||||||
this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true;
|
this.messageCache.ackCache(messageID);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,4 +122,9 @@ class ContactListState extends ChangeNotifier {
|
||||||
int idx = _contacts.indexWhere((element) => element.onion == byHandle);
|
int idx = _contacts.indexWhere((element) => element.onion == byHandle);
|
||||||
return idx >= 0 ? _contacts[idx] : null;
|
return idx >= 0 ? _contacts[idx] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) {
|
||||||
|
getContact(identifier)?.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation);
|
||||||
|
updateLastMessageTime(identifier, DateTime.now());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:cwtch/config.dart';
|
import 'package:cwtch/config.dart';
|
||||||
|
import 'package:cwtch/cwtch/cwtch.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
|
import 'messagecache.dart';
|
||||||
import 'messages/filemessage.dart';
|
import 'messages/filemessage.dart';
|
||||||
import 'messages/invitemessage.dart';
|
import 'messages/invitemessage.dart';
|
||||||
import 'messages/malformedmessage.dart';
|
import 'messages/malformedmessage.dart';
|
||||||
|
@ -28,7 +31,9 @@ const GroupConversationHandleLength = 32;
|
||||||
|
|
||||||
abstract class Message {
|
abstract class Message {
|
||||||
MessageMetadata getMetadata();
|
MessageMetadata getMetadata();
|
||||||
|
|
||||||
Widget getWidget(BuildContext context, Key key);
|
Widget getWidget(BuildContext context, Key key);
|
||||||
|
|
||||||
Widget getPreviewWidget(BuildContext context);
|
Widget getPreviewWidget(BuildContext context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,29 +62,110 @@ Message compileOverlay(MessageMetadata metadata, String messageData) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) {
|
abstract class CacheHandler {
|
||||||
|
MessageInfo? lookup(MessageCache cache);
|
||||||
|
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier);
|
||||||
|
void add(MessageCache cache, MessageInfo messageInfo, String contenthash);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ByIndex implements CacheHandler {
|
||||||
|
int index;
|
||||||
|
|
||||||
|
ByIndex(this.index);
|
||||||
|
|
||||||
|
MessageInfo? lookup(MessageCache cache) {
|
||||||
|
return cache.getByIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||||
|
return cwtch.GetMessage(profileOnion, conversationIdentifier, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||||
|
cache.add(messageInfo, index, contenthash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ById implements CacheHandler {
|
||||||
|
int id;
|
||||||
|
|
||||||
|
ById(this.id);
|
||||||
|
|
||||||
|
MessageInfo? lookup(MessageCache cache) {
|
||||||
|
return cache.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||||
|
return cwtch.GetMessageByID(profileOnion, conversationIdentifier, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||||
|
cache.addUnindexed(messageInfo, contenthash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ByContentHash implements CacheHandler {
|
||||||
|
String hash;
|
||||||
|
|
||||||
|
ByContentHash(this.hash);
|
||||||
|
|
||||||
|
MessageInfo? lookup(MessageCache cache) {
|
||||||
|
return cache.getByContentHash(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||||
|
return cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||||
|
cache.addUnindexed(messageInfo, contenthash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) {
|
||||||
|
var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false);
|
||||||
|
// Hit cache
|
||||||
|
MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, cacheHandler);
|
||||||
|
if (messageInfo != null) {
|
||||||
|
return Future.value(compileOverlay(messageInfo.metadata, messageInfo.wrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and Cache
|
||||||
|
var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler);
|
||||||
|
return messageInfoFuture.then((MessageInfo? messageInfo) {
|
||||||
|
if (messageInfo != null) {
|
||||||
|
return compileOverlay(messageInfo.metadata, messageInfo.wrapper);
|
||||||
|
} else {
|
||||||
|
return MalformedMessage(malformedMetadata);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) {
|
||||||
|
// Hit cache
|
||||||
try {
|
try {
|
||||||
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
|
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
|
||||||
if (cache != null && cache.length > index) {
|
if (cache != null) {
|
||||||
if (cache[index] != null) {
|
MessageInfo? messageInfo = cacheHandler.lookup(cache);
|
||||||
return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper));
|
if (messageInfo != null) {
|
||||||
|
return messageInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
|
||||||
// provider check failed...make an expensive call...
|
// provider check failed...make an expensive call...
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MessageInfo?> fetchAndCacheMessageInfo(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) {
|
||||||
|
// Load and cache
|
||||||
try {
|
try {
|
||||||
Future<dynamic> rawMessageEnvelopeFuture;
|
Future<dynamic> rawMessageEnvelopeFuture;
|
||||||
|
|
||||||
if (byID) {
|
rawMessageEnvelopeFuture = cacheHandler.fetch(Provider.of<FlwtchState>(context, listen: false).cwtch, profileOnion, conversationIdentifier);
|
||||||
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, index);
|
|
||||||
} else {
|
|
||||||
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
|
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
|
||||||
var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", <String, String>{}, false, true, false);
|
|
||||||
try {
|
try {
|
||||||
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
|
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
|
||||||
// There are 2 conditions in which this error condition can be met:
|
// There are 2 conditions in which this error condition can be met:
|
||||||
|
@ -94,7 +180,7 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, int co
|
||||||
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
|
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
|
||||||
return Future.delayed(Duration(seconds: 2), () {
|
return Future.delayed(Duration(seconds: 2), () {
|
||||||
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
|
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
|
||||||
return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value);
|
return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,16 +193,25 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, int co
|
||||||
var ackd = messageWrapper['Acknowledged'];
|
var ackd = messageWrapper['Acknowledged'];
|
||||||
var error = messageWrapper['Error'] != null;
|
var error = messageWrapper['Error'] != null;
|
||||||
var signature = messageWrapper['Signature'];
|
var signature = messageWrapper['Signature'];
|
||||||
metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false);
|
var contenthash = messageWrapper['ContentHash'];
|
||||||
|
var localIndex = messageWrapper['LocalIndex'];
|
||||||
|
var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false);
|
||||||
|
var messageInfo = new MessageInfo(metadata, messageWrapper['Message']);
|
||||||
|
|
||||||
return compileOverlay(metadata, messageWrapper['Message']);
|
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
|
||||||
|
if (cache != null) {
|
||||||
|
cacheHandler.add(cache, messageInfo, contenthash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageInfo;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
EnvironmentConfig.debugLog("an error! " + e.toString());
|
EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString());
|
||||||
return MalformedMessage(metadata);
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", <String, String>{}, false, true, false)));
|
EnvironmentConfig.debugLog("message handler exeption on get message: $e");
|
||||||
|
return Future.value(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,12 +234,14 @@ class MessageMetadata extends ChangeNotifier {
|
||||||
dynamic get attributes => this._attributes;
|
dynamic get attributes => this._attributes;
|
||||||
|
|
||||||
bool get ackd => this._ackd;
|
bool get ackd => this._ackd;
|
||||||
|
|
||||||
set ackd(bool newVal) {
|
set ackd(bool newVal) {
|
||||||
this._ackd = newVal;
|
this._ackd = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get error => this._error;
|
bool get error => this._error;
|
||||||
|
|
||||||
set error(bool newVal) {
|
set error(bool newVal) {
|
||||||
this._error = newVal;
|
this._error = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
|
@ -1,7 +1,58 @@
|
||||||
import 'message.dart';
|
import 'message.dart';
|
||||||
|
|
||||||
class MessageCache {
|
class MessageInfo {
|
||||||
final MessageMetadata metadata;
|
final MessageMetadata metadata;
|
||||||
final String wrapper;
|
final String wrapper;
|
||||||
MessageCache(this.metadata, this.wrapper);
|
MessageInfo(this.metadata, this.wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageCache {
|
||||||
|
late Map<int, MessageInfo> cache;
|
||||||
|
late List<int?> cacheByIndex;
|
||||||
|
late Map<String, int> cacheByHash;
|
||||||
|
|
||||||
|
MessageCache() {
|
||||||
|
cache = {};
|
||||||
|
cacheByIndex = List.empty(growable: true);
|
||||||
|
cacheByHash = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int get indexedLength => cacheByIndex.length;
|
||||||
|
|
||||||
|
MessageInfo? getById(int id) => cache[id];
|
||||||
|
MessageInfo? getByIndex(int index) {
|
||||||
|
if (index >= cacheByIndex.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cache[cacheByIndex[index]];
|
||||||
|
}
|
||||||
|
|
||||||
|
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), data);
|
||||||
|
this.cacheByIndex.insert(0, messageID);
|
||||||
|
if (contenthash != null && contenthash != "") {
|
||||||
|
this.cacheByHash[contenthash] = messageID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(MessageInfo messageInfo, int index, String? contenthash) {
|
||||||
|
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||||
|
this.cacheByIndex.insert(index, messageInfo.metadata.messageID);
|
||||||
|
if (contenthash != null && contenthash != "") {
|
||||||
|
this.cacheByHash[contenthash] = messageInfo.metadata.messageID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addUnindexed(MessageInfo messageInfo, String? contenthash) {
|
||||||
|
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||||
|
if (contenthash != null && contenthash != "") {
|
||||||
|
this.cacheByHash[contenthash] = messageInfo.metadata.messageID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ackCache(int messageID) {
|
||||||
|
cache[messageID]?.metadata.ackd = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
|
import '../messagecache.dart';
|
||||||
|
import '../profile.dart';
|
||||||
|
|
||||||
class QuotedMessageStructure {
|
class QuotedMessageStructure {
|
||||||
final String quotedHash;
|
final String quotedHash;
|
||||||
|
@ -21,22 +23,6 @@ class QuotedMessageStructure {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocallyIndexedMessage {
|
|
||||||
final dynamic message;
|
|
||||||
final int index;
|
|
||||||
|
|
||||||
LocallyIndexedMessage(this.message, this.index);
|
|
||||||
|
|
||||||
LocallyIndexedMessage.fromJson(Map<String, dynamic> json)
|
|
||||||
: message = json['Message'],
|
|
||||||
index = json['LocalIndex'];
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'Message': message,
|
|
||||||
'LocalIndex': index,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class QuotedMessage extends Message {
|
class QuotedMessage extends Message {
|
||||||
final MessageMetadata metadata;
|
final MessageMetadata metadata;
|
||||||
final String content;
|
final String content;
|
||||||
|
@ -70,35 +56,10 @@ class QuotedMessage extends Message {
|
||||||
return MalformedBubble();
|
return MalformedBubble();
|
||||||
}
|
}
|
||||||
|
|
||||||
var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]);
|
|
||||||
Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) {
|
|
||||||
if (matchingMessages == "[]") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// reverse order the messages from newest to oldest and return the
|
|
||||||
// first matching message where it's index is less than the index of this
|
|
||||||
// message
|
|
||||||
try {
|
|
||||||
var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList();
|
|
||||||
LocallyIndexedMessage candidate = list.reversed.first;
|
|
||||||
return candidate;
|
|
||||||
} catch (e) {
|
|
||||||
// Malformed Message will be returned...
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ChangeNotifierProvider.value(
|
return ChangeNotifierProvider.value(
|
||||||
value: this.metadata,
|
value: this.metadata,
|
||||||
builder: (bcontext, child) {
|
builder: (bcontext, child) {
|
||||||
return MessageRow(
|
return MessageRow(QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, ByContentHash(message["quotedHash"]))), key: key);
|
||||||
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
|
|
||||||
if (localIndex != null) {
|
|
||||||
return messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index);
|
|
||||||
}
|
|
||||||
return MalformedMessage(this.metadata);
|
|
||||||
})),
|
|
||||||
key: key);
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return MalformedBubble();
|
return MalformedBubble();
|
||||||
|
|
|
@ -132,7 +132,6 @@ class ProfileInfoState extends ChangeNotifier {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
print("profileinfostate.dispose()");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) {
|
void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) {
|
||||||
|
|
|
@ -225,8 +225,11 @@ class _MessageViewState extends State<MessageView> {
|
||||||
ctrlrCompose.clear();
|
ctrlrCompose.clear();
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
Future.delayed(const Duration(milliseconds: 80), () {
|
Future.delayed(const Duration(milliseconds: 80), () {
|
||||||
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(Provider.of<ContactInfoState>(context, listen: false).identifier)?.bumpMessageCache();
|
var profile = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
|
||||||
|
var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
|
||||||
|
fetchAndCacheMessageInfo(context, profile, identifier, ByIndex(0));
|
||||||
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
|
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
|
||||||
|
Provider.of<ContactInfoState>(context, listen: false).totalMessages += 1;
|
||||||
// Resort the contact list...
|
// Resort the contact list...
|
||||||
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).identifier, DateTime.now());
|
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).identifier, DateTime.now());
|
||||||
});
|
});
|
||||||
|
@ -281,8 +284,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
var children;
|
var children;
|
||||||
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
|
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
|
||||||
var quoted = FutureBuilder(
|
var quoted = FutureBuilder(
|
||||||
future:
|
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, ById(Provider.of<AppState>(context).selectedIndex!)),
|
||||||
messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!, byID: true),
|
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
var message = snapshot.data! as Message;
|
var message = snapshot.data! as Message;
|
||||||
|
|
|
@ -83,7 +83,7 @@ class _MessageListState extends State<MessageList> {
|
||||||
var messageIndex = index;
|
var messageIndex = index;
|
||||||
|
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
|
future: messageHandler(outerContext, profileOnion, contactHandle, ByIndex(messageIndex)),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
var message = snapshot.data as Message;
|
var message = snapshot.data as Message;
|
||||||
|
|
|
@ -220,8 +220,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
)))));
|
)))));
|
||||||
var mark = Provider.of<ContactInfoState>(context).newMarker;
|
var mark = Provider.of<ContactInfoState>(context).newMarker;
|
||||||
if (mark > 0 &&
|
if (mark > 0 &&
|
||||||
Provider.of<ContactInfoState>(context).messageCache.length > mark &&
|
Provider.of<ContactInfoState>(context).messageCache.indexedLength > mark &&
|
||||||
Provider.of<ContactInfoState>(context).messageCache[mark - 1]?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
|
Provider.of<ContactInfoState>(context).messageCache.getByIndex(mark - 1)?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
|
||||||
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
|
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
|
||||||
} else {
|
} else {
|
||||||
return mr;
|
return mr;
|
||||||
|
|
Loading…
Reference in New Issue
why doesn't this notify listeners?