New Cwtch Library Integration #258

Merged
sarah merged 14 commits from cwtch-lib-integration into trunk 2021-12-10 21:02:02 +00:00
13 changed files with 137 additions and 67 deletions
Showing only changes of commit c9319d32d0 - Show all commits

View File

@ -184,6 +184,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val indexI = a.getInt("index").toLong()
Log.i("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString())
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build())
}
"GetMessageByID" -> {

View File

@ -30,6 +30,7 @@ import android.os.Build
import android.os.Environment
import android.database.Cursor
import android.provider.MediaStore
import cwtch.Cwtch
class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView()

View File

@ -130,6 +130,10 @@ class CwtchNotifier {
case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!");
var identifier = int.parse(data["ConversationID"]);
var messageID = int.parse(data["Index"]);
var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
// We might not have received a contact created for this contact yet...
// In that case the **next** event we receive will actually update these values...
@ -140,6 +144,7 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now());
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"], "");
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++;
// We only ever see messages from authenticated peers.
@ -156,11 +161,12 @@ class CwtchNotifier {
break;
case "IndexedAcknowledgement":
var conversation = int.parse(data["ConversationID"]);
var message_index = int.parse(data["Index"]);
var messageID = int.parse(data["Index"]);
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation);
// We return -1 for protocol message acks if there is no message
if (message_index == -1) break;
var key = contact!.getMessageKeyOrFail(conversation, message_index, contact.lastMessageTime);
if (messageID == -1) break;
var key = contact!.getMessageKeyOrFail(conversation, messageID, contact.lastMessageTime);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
@ -181,11 +187,19 @@ class CwtchNotifier {
var identifier = int.parse(data["ConversationID"]);
if (data["ProfileOnion"] != data["RemotePeer"]) {
var idx = int.parse(data["Index"]);
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages;
// 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) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages = idx + 1;
profileCN
.getProfile(data["ProfileOnion"])
?.contactList
.getContact(identifier)!
.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"], data["Signature"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++;
//if not currently open
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
@ -194,7 +208,6 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
}
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
// 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
// Received refers to the time we actually saw the message from the server

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/profileservers.dart';
@ -501,6 +502,12 @@ ContactAuthorization stringToContactAuthorization(String authStr) {
}
}
class MessageCache {
final MessageMetadata metadata;
final String wrapper;
MessageCache(this.metadata, this.wrapper);
}
class ContactInfoState extends ChangeNotifier {
final String profileOnion;
final int identifier;
@ -515,6 +522,7 @@ class ContactInfoState extends ChangeNotifier {
late int _totalMessages = 0;
late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageRowState>> keys;
late List<MessageCache?> messageCache;
int _newMarker = 0;
DateTime _newMarkerClearAt = DateTime.now();
@ -546,6 +554,7 @@ class ContactInfoState extends ChangeNotifier {
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server;
this._archived = archived;
this.messageCache = List.empty(growable: true);
keys = Map<String, GlobalKey<MessageRowState>>();
}
@ -677,4 +686,12 @@ class ContactInfoState extends ChangeNotifier {
GlobalKey<MessageRowState> ret = keys[index]!;
return ret;
}
void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data, String signature) {
this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, signature, {}, false, false), data));
}
void bumpMessageCache() {
sarah marked this conversation as resolved
Review

ideally internal. the public setter for totalMessages can call, and updateMessageCaches should internally inc totalMessages

ideally internal. the public setter for totalMessages can call, and updateMessageCaches should internally inc totalMessages
this.messageCache.insert(0, null);
}
}

View File

@ -28,11 +28,43 @@ const GroupConversationHandleLength = 32;
abstract class Message {
MessageMetadata getMetadata();
Widget getWidget(BuildContext context);
Widget getWidget(BuildContext context, Key key);
Widget getPreviewWidget(BuildContext context);
}
Message compileOverlay(MessageMetadata metadata, String messageData) {
try {
dynamic message = jsonDecode(messageData);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
}
} catch (e) {
return MalformedMessage(metadata);
}
}
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) {
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)!.messageCache;
if (cache.length > index) {
if (cache[index] != null) {
return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper));
}
}
try {
Future<dynamic> rawMessageEnvelopeFuture;
@ -43,7 +75,7 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, int co
}
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", <String, String>{}, false, true);
var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", <String, String>{}, false, true);
try {
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
// There are 2 conditions in which this error condition can be met:
@ -58,7 +90,7 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, int co
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
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.");
return messageHandler(context, profileOnion, conversationIdentifier, index, byID: byID).then((value) => value);
return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value);
});
}
@ -71,33 +103,16 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, int co
var ackd = messageWrapper['Acknowledged'];
var error = messageWrapper['Error'] != null;
var signature = messageWrapper['Signature'];
metadata = MessageMetadata(profileOnion, conversationIdentifier, index, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error);
metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error);
dynamic message = jsonDecode(messageWrapper['Message']);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
}
return compileOverlay(metadata, messageWrapper['Message']);
} catch (e) {
EnvironmentConfig.debugLog("an error! " + e.toString());
return MalformedMessage(metadata);
}
});
} catch (e) {
return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", <String, String>{}, false, true)));
return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", <String, String>{}, false, true)));
}
}
@ -105,7 +120,6 @@ class MessageMetadata extends ChangeNotifier {
// meta-metadata
final String profileOnion;
final int conversationIdentifier;
final int messageIndex;
final int messageID;
final DateTime timestamp;
@ -131,6 +145,5 @@ class MessageMetadata extends ChangeNotifier {
notifyListeners();
}
MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd,
this._error);
MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error);
}

View File

@ -17,8 +17,9 @@ class FileMessage extends Message {
FileMessage(this.metadata, this.content);
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
key: key,
value: this.metadata,
builder: (bcontext, child) {
dynamic shareObj = jsonDecode(this.content);
@ -34,9 +35,7 @@ class FileMessage extends Message {
return MessageRow(MalformedBubble());
}
var lrt = Provider.of<ContactInfoState>(bcontext).lastMessageTime;
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt));
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize));
});
}

View File

@ -17,8 +17,9 @@ class InviteMessage extends Message {
InviteMessage(this.overlay, this.metadata, this.content);
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
key: key,
value: this.metadata,
builder: (bcontext, child) {
String inviteTarget;
@ -40,8 +41,7 @@ class InviteMessage extends Message {
}
}
var lrt = Provider.of<ContactInfoState>(bcontext).lastMessageTime;
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt));
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite));
});
}

View File

@ -9,8 +9,9 @@ class MalformedMessage extends Message {
MalformedMessage(this.metadata);
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
key: key,
value: this.metadata,
builder: (context, child) {
return MessageRow(MalformedBubble());

View File

@ -51,7 +51,7 @@ class QuotedMessage extends Message {
dynamic message = jsonDecode(this.content);
return Text(message["body"]);
} catch (e) {
return MalformedMessage(this.metadata).getWidget(context);
return MalformedMessage(this.metadata).getWidget(context, Key("malformed"));
}
});
}
@ -62,16 +62,15 @@ class QuotedMessage extends Message {
}
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
try {
dynamic message = jsonDecode(this.content);
if (message["body"] == null || message["quotedHash"] == null) {
return MalformedMessage(this.metadata).getWidget(context);
return MalformedMessage(this.metadata).getWidget(context, key);
}
var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]);
int messageIndex = metadata.messageIndex;
Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) {
if (matchingMessages == "[]") {
return null;
@ -81,9 +80,7 @@ class QuotedMessage extends Message {
// message
try {
var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList();
LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () {
return list.firstWhere((element) => messageIndex > element.index);
});
LocallyIndexedMessage candidate = list.reversed.first;
return candidate;
} catch (e) {
// Malformed Message will be returned...
@ -92,20 +89,18 @@ class QuotedMessage extends Message {
});
return ChangeNotifierProvider.value(
key: key,
value: this.metadata,
builder: (bcontext, child) {
var lrt = Provider.of<ContactInfoState>(bcontext).lastMessageTime;
return MessageRow(
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
if (localIndex != null) {
return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index);
}
return MalformedMessage(this.metadata);
})),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt));
return MessageRow(QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
if (localIndex != null) {
return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index);
}
return MalformedMessage(this.metadata);
})));
});
} catch (e) {
return MalformedMessage(this.metadata).getWidget(context);
return MalformedMessage(this.metadata).getWidget(context, key);
}
}
}

View File

@ -31,14 +31,15 @@ class TextMessage extends Message {
}
@override
Widget getWidget(BuildContext context) {
Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value(
key: key,
value: this.metadata,
builder: (bcontext, child) {
var lrt = Provider.of<ContactInfoState>(bcontext).lastMessageTime;
var key = Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt);
// var key = Provider.of<ContactInfoState>(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt);
return MessageRow(MessageBubble(this.content), key: key);
return MessageRow(MessageBubble(this.content));
});
}
}

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/quotedmessage.dart';
@ -213,6 +214,7 @@ class _MessageViewState extends State<MessageView> {
ctrlrCompose.clear();
focusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 80), () {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(Provider.of<ContactInfoState>(context, listen: false).identifier)?.bumpMessageCache();
Provider.of<ContactInfoState>(context, listen: false).totalMessages++;
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
// Resort the contact list...

View File

@ -79,13 +79,15 @@ class _MessageListState extends State<MessageList> {
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).identifier;
var messageIndex = index;
// var key = Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, Provider.of<ContactInfoState>(outerContext).totalMessages - index, DateTime.now());
return FutureBuilder(
//key: Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, Provider.of<ContactInfoState>(outerContext).totalMessages - index, DateTime.now()),
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
builder: (context, snapshot) {
if (snapshot.hasData) {
var message = snapshot.data as Message;
// Already includes MessageRow,,
return message.getWidget(context);
var key = Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID, DateTime.now());
return message.getWidget(context, key);
} else {
return MessageLoadingBubble();
}
@ -97,3 +99,23 @@ class _MessageListState extends State<MessageList> {
])));
}
}
class CachedMessage extends Message {
@override
MessageMetadata getMetadata() {
// TODO: implement getMetadata
throw UnimplementedError();
}
@override
Widget getPreviewWidget(BuildContext context) {
// TODO: implement getPreviewWidget
throw UnimplementedError();
}
@override
Widget getWidget(BuildContext context, Key key) {
// TODO: implement getWidget
throw UnimplementedError();
}
}

View File

@ -34,7 +34,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
@override
void initState() {
super.initState();
index = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
index = Provider.of<MessageMetadata>(context, listen: false).messageID;
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
@ -75,7 +75,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
}
Widget wdgIcons = Visibility(
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageIndex,
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
@ -169,7 +169,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
// For desktop...
onHover: (event) {
setState(() {
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
});
},
onExit: (event) {
@ -204,7 +204,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
children: widgetRow,
)))));
var mark = Provider.of<ContactInfoState>(context).newMarker;
if (mark > 0 && mark == Provider.of<MessageMetadata>(context).messageIndex + 1) {
if (mark > 0 && Provider.of<ContactInfoState>(context).messageCache[mark]?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
} else {
return mr;
@ -251,12 +251,17 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
}
void _btnGoto() {
selectConversation(context, Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier);
var id = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle)?.identifier;
if (id == null) {
// Can't happen
} else {
selectConversation(context, id);
}
}
void _btnAdd() {
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") {
if (sender == "") {
print("sender not yet loaded");
return;
}