Quoted Messages Initial Pass

This commit is contained in:
Sarah Jamie Lewis 2021-07-05 12:31:16 -07:00
parent 370a7fd1da
commit b9984a3598
11 changed files with 273 additions and 109 deletions

View File

@ -122,6 +122,12 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val indexI = a.getInt("index") val indexI = a.getInt("index")
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build())
} }
"GetMessageByContentHash" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val contentHash = (a.get("contentHash") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.getMessageByContentHash(profile, handle, contentHash)).build())
}
"UpdateMessageFlags" -> { "UpdateMessageFlags" -> {
val profile = (a.get("profile") as? String) ?: "" val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: "" val handle = (a.get("contact") as? String) ?: ""

View File

@ -32,6 +32,8 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index); Future<dynamic> GetMessage(String profile, String handle, int index);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessageByContentHash(String profile, String handle, String contentHash);
// ignore: non_constant_identifier_names
void UpdateMessageFlags(String profile, String handle, int index, int flags); void UpdateMessageFlags(String profile, String handle, int index, int flags);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profile, String handle, String message); void SendMessage(String profile, String handle, String message);

View File

@ -57,9 +57,9 @@ typedef GetIntFromStrStrFn = int Function(Pointer<Utf8>, int, Pointer<Utf8>, int
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32); typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int); typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
//func GetMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, start C.int, end C.int) *C.char { // func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char
typedef get_json_blob_from_str_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32); typedef get_json_blob_from_str_str_str_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int); typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef appbus_events_function = Pointer<Utf8> Function(); typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function(); typedef AppbusEventsFn = Pointer<Utf8> Function();
@ -397,4 +397,17 @@ class CwtchFfi implements Cwtch {
_receivePort.close(); _receivePort.close();
print("Receive Port Closed"); print("Receive Port Closed");
} }
@override
Future GetMessageByContentHash(String profile, String handle, String contentHash) async {
var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_str_str_function>>("c_GetMessagesByContentHash");
// ignore: non_constant_identifier_names
final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrStrStrFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
final utf8contentHash = contentHash.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length);
String jsonMessage = jsonMessageBytes.toDartString();
return jsonMessage;
}
} }

View File

@ -183,4 +183,9 @@ class CwtchGomobile implements Cwtch {
print("gomobile.dart Shutdown"); print("gomobile.dart Shutdown");
cwtchPlatform.invokeMethod("Shutdown", {}); cwtchPlatform.invokeMethod("Shutdown", {});
} }
@override
Future GetMessageByContentHash(String profile, String handle, String contentHash) {
return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "contentHash": contentHash});
}
} }

View File

@ -67,6 +67,7 @@ class AppState extends ChangeNotifier {
String appError = ""; String appError = "";
String? _selectedProfile; String? _selectedProfile;
String? _selectedConversation; String? _selectedConversation;
int? _selectedIndex;
void SetCwtchInit() { void SetCwtchInit() {
cwtchInit = true; cwtchInit = true;
@ -90,6 +91,12 @@ class AppState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
int? get selectedIndex => _selectedIndex;
set selectedIndex(int? newVal) {
this._selectedIndex = newVal;
notifyListeners();
}
bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height;
} }
@ -457,7 +464,7 @@ class MessageState extends ChangeNotifier {
final String profileOnion; final String profileOnion;
final String contactHandle; final String contactHandle;
final int messageIndex; final int messageIndex;
late String _message; late dynamic _message;
late int _overlay; late int _overlay;
late String _inviteTarget; late String _inviteTarget;
late String _inviteNick; late String _inviteNick;
@ -535,7 +542,7 @@ class MessageState extends ChangeNotifier {
return; return;
} }
dynamic message = jsonDecode(messageWrapper['Message']); dynamic message = jsonDecode(messageWrapper['Message']);
this._message = message['d']; this._message = message['d'] as dynamic;
this._overlay = int.parse(message['o'].toString()); this._overlay = int.parse(message['o'].toString());
this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
this._senderOnion = messageWrapper['PeerID']; this._senderOnion = messageWrapper['PeerID'];

View File

@ -1,7 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -104,6 +105,27 @@ class _MessageViewState extends State<MessageView> {
void _sendMessage([String? ignoredParam]) { void _sendMessage([String? ignoredParam]) {
if (ctrlrCompose.value.text.isNotEmpty) { if (ctrlrCompose.value.text.isNotEmpty) {
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
Provider.of<FlwtchState>(context)
.cwtch
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!).then((data) {
try {
var messageWrapper = jsonDecode(data! as String);
var bytes1 = utf8.encode(messageWrapper["PeerID"]+messageWrapper['Message']);
var digest1 = sha256.convert(bytes1);
var contentHash = base64Encode(digest1.bytes);
var quotedMessage = "{\"quotedHash\":\""+contentHash+"\",\"body\":\""+ctrlrCompose.value.text+"\"}";
ChatMessage cm = new ChatMessage(o: 10, d: quotedMessage);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
} catch (e) {
}
Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper();
});
} else {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
@ -111,6 +133,7 @@ class _MessageViewState extends State<MessageView> {
_sendMessageHelper(); _sendMessageHelper();
} }
} }
}
void _sendInvitation([String? ignoredParam]) { void _sendInvitation([String? ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
@ -130,7 +153,7 @@ class _MessageViewState extends State<MessageView> {
} }
Widget _buildComposeBox() { Widget _buildComposeBox() {
return Container( var composeBox = Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor(), color: Provider.of<Settings>(context).theme.backgroundMainColor(),
padding: EdgeInsets.all(2), padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2), margin: EdgeInsets.all(2),
@ -174,6 +197,40 @@ class _MessageViewState extends State<MessageView> {
], ],
), ),
); );
var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
var quoted = FutureBuilder(
future: Provider.of<FlwtchState>(context)
.cwtch
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!),
builder: (context, snapshot) {
if (snapshot.hasData) {
try {
var messageWrapper = jsonDecode(snapshot.data! as String);
dynamic message = jsonDecode(messageWrapper['Message']);
return Container(
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
color: messageWrapper['PeerID'] != Provider.of<AppState>(context).selectedProfile
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
child: Text(message["d"]));
} catch (e) {
return MalformedBubble();
}
} else {
return Text("");
}
},
);
children = [quoted, composeBox];
} else {
children = [composeBox];
}
return Column(mainAxisSize: MainAxisSize.min, children: children);
} }
// Send the message if enter is pressed without the shift key... // Send the message if enter is pressed without the shift key...
@ -181,11 +238,8 @@ class _MessageViewState extends State<MessageView> {
var data = event.data as RawKeyEventData; var data = event.data as RawKeyEventData;
if (data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) { if (data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) {
final messageWithoutNewLine = ctrlrCompose.value.text.trimRight(); final messageWithoutNewLine = ctrlrCompose.value.text.trimRight();
ctrlrCompose.value = TextEditingValue( ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine);
text: messageWithoutNewLine
);
_sendMessage(); _sendMessage();
} }
} }

View File

@ -98,6 +98,7 @@ class _ContactRowState extends State<ContactRow> {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0; Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView // triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).selectedConversation = contact.onion; Provider.of<AppState>(context, listen: false).selectedConversation = contact.onion;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
// if in singlepane mode, push to the stack // if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context); var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion); if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion);

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/widgets/quotedmessage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -33,14 +34,18 @@ class _MessageRowState extends State<MessageRow> {
Widget wdgBubble = Widget wdgBubble =
Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble()); Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble());
Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of<Settings>(context).theme.dropShadowColor()); Widget wdgIcons = IconButton(
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageState>(context).messageIndex;
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor()));
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[]; var widgetRow = <Widget>[];
if (fromMe) { if (fromMe) {
widgetRow = <Widget>[ widgetRow = <Widget>[
wdgSpacer, wdgSpacer,
//wdgIcons, wdgIcons,
wdgBubble, wdgBubble,
]; ];
} else { } else {
@ -60,7 +65,7 @@ class _MessageRowState extends State<MessageRow> {
widgetRow = <Widget>[ widgetRow = <Widget>[
wdgPortrait, wdgPortrait,
wdgBubble, wdgBubble,
//wdgIcons, wdgIcons,
wdgSpacer, wdgSpacer,
]; ];
} }
@ -75,6 +80,8 @@ class _MessageRowState extends State<MessageRow> {
case 100: case 100:
case 101: case 101:
return InvitationBubble(); return InvitationBubble();
case 10:
return QuotedMessageBubble();
} }
return MalformedBubble(); return MalformedBubble();
} }

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:cwtch/main.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -7,6 +10,22 @@ import 'package:intl/intl.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.dart'; import 'messagebubbledecorations.dart';
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 QuotedMessageBubble extends StatefulWidget { class QuotedMessageBubble extends StatefulWidget {
@override @override
QuotedMessageBubbleState createState() => QuotedMessageBubbleState(); QuotedMessageBubbleState createState() => QuotedMessageBubbleState();
@ -22,6 +41,27 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString(); var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
try {
dynamic message = jsonDecode(Provider.of<MessageState>(context).message);
var quotedMessagePotentials =
Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(Provider.of<MessageState>(context).profileOnion, Provider.of<MessageState>(context).contactHandle, message["quotedHash"]);
int messageIndex = Provider.of<MessageState>(context).messageIndex;
var quotedMessage = quotedMessagePotentials.then((matchingMessages) {
// 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.firstWhere((element) => messageIndex < element.index, orElse: () {
return list.firstWhere((element) => messageIndex > element.index);
});
return candidate;
} catch (e) {
// Malformed Message will be returned...
}
});
if (Provider.of<MessageState>(context).timestamp != null) { if (Provider.of<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo // user-configurable timestamps prolly ideal? #todo
DateTime messageDate = Provider.of<MessageState>(context).timestamp; DateTime messageDate = Provider.of<MessageState>(context).timestamp;
@ -42,7 +82,7 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())); style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText( var wdgMessage = SelectableText(
(Provider.of<MessageState>(context).message ?? "") + '\u202F', (message["body"] ?? "") + '\u202F',
key: Key(myKey), key: Key(myKey),
focusNode: _focus, focusNode: _focus,
style: TextStyle( style: TextStyle(
@ -52,12 +92,30 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
textWidthBasis: TextWidthBasis.longestLine, textWidthBasis: TextWidthBasis.longestLine,
); );
var wdgQuote = FutureBuilder(
future: quotedMessage,
builder: (context, snapshot) {
if (snapshot.hasData) {
var lim = (snapshot.data! as LocallyIndexedMessage);
var limmessage = lim.message;
// Swap the background color for quoted tweets..
return Container(
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor() : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
child: Text(jsonDecode(limmessage)["d"]));
} else {
// This should be almost instantly resolved, any failure likely means an issue in decoding...
return MalformedBubble();
}
},
);
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate); var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var error = Provider.of<MessageState>(context).error; var error = Provider.of<MessageState>(context).error;
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString());
return RepaintBoundary( return RepaintBoundary(
child: Container( child: Container(
child: Container( child: Container(
@ -83,7 +141,10 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); children: fromMe ? [wdgQuote, wdgMessage, wdgDecorations] : [wdgSender, wdgQuote, wdgMessage, wdgDecorations])))));
}); });
} catch (e) {
return MalformedBubble();
}
} }
} }

View File

@ -57,6 +57,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
crypto:
dependency: "direct main"
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -35,6 +35,7 @@ dependencies:
ffi: ^1.0.0 ffi: ^1.0.0
path_provider: ^2.0.0 path_provider: ^2.0.0
desktop_notifications: 0.5.0 desktop_notifications: 0.5.0
crypto: 3.0.1
glob: any glob: any
flutter_test: flutter_test: