From 3b67e9d35826cc8f5fe3cd7e911b75accafaaceb Mon Sep 17 00:00:00 2001 From: erinn Date: Sun, 2 May 2021 17:28:35 -0700 Subject: [PATCH 1/3] set unreadmessages to zero on read --- lib/widgets/messagelist.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index c4b7868..675758a 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -26,6 +26,7 @@ class _MessageListState extends State { if (force || Provider.of(context, listen: false).totalMessages > lastMessageCount) { if (ctrlr1.position.pixels == lastMaxScroll) { ctrlr1.jumpTo(ctrlr1.position.maxScrollExtent); + Provider.of(context, listen: false).unreadMessages = 0; } setState(() { -- 2.25.1 From 0116db8dc484bb2e94b0f805140b4e6418b1d4c5 Mon Sep 17 00:00:00 2001 From: erinn Date: Sun, 2 May 2021 19:38:43 -0700 Subject: [PATCH 2/3] live acks for peer messages --- lib/cwtch/cwtchNotifier.dart | 9 +++++++++ lib/model.dart | 10 ++++++++++ lib/widgets/messagebubble.dart | 4 ++-- lib/widgets/messagelist.dart | 2 +- lib/widgets/messagerow.dart | 2 ++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index c5be273..e25c239 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:provider/provider.dart'; import 'package:flutter_app/torstatus.dart'; +import 'package:flutter_app/widgets/messagebubble.dart'; import '../errorHandler.dart'; import '../model.dart'; @@ -62,6 +64,13 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).totalMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now()); break; + case "IndexedAcknowledgement": + var idx = int.parse(data["Index"]); + if (idx < 0) break; + var key = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).getMessageKey(idx); + if (key == null) break; + Provider.of(key.currentContext, listen: false).ackd = true; + break; case "NewMessageFromGroup": profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).unreadMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).totalMessages++; diff --git a/lib/model.dart b/lib/model.dart index b9c1111..6a5fa49 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter_app/models/servers.dart'; +import 'package:flutter_app/widgets/messagebubble.dart'; import 'package:provider/provider.dart'; import 'dart:async'; import 'dart:collection'; @@ -232,6 +233,7 @@ class ContactInfoState extends ChangeNotifier { int _unreadMessages = 0; int _totalMessages = 0; DateTime _lastMessageTime; + Map keys; // todo: a nicer way to model contacts, groups and other "entities" bool _isGroup; @@ -263,6 +265,7 @@ class ContactInfoState extends ChangeNotifier { this._savePeerHistory = savePeerHistory; this._lastMessageTime = lastMessageTime; this._server = server; + keys = Map(); } get nickname => this._nickname; @@ -336,6 +339,13 @@ class ContactInfoState extends ChangeNotifier { return this.status == "Authenticated"; } } + + GlobalKey getMessageKey(int index) { + if (keys[index] == null) { + keys[index] = GlobalKey(); + } + return keys[index]; + } } class MessageState extends ChangeNotifier { diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 2fbf4cb..3f80ca1 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -7,10 +7,10 @@ import '../settings.dart'; class MessageBubble extends StatefulWidget { @override - _MessageBubbleState createState() => _MessageBubbleState(); + MessageBubbleState createState() => MessageBubbleState(); } -class _MessageBubbleState extends State { +class MessageBubbleState extends State { FocusNode _focus = FocusNode(); @override diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 675758a..b75842c 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -64,7 +64,7 @@ class _MessageListState extends State { contactHandle: Provider.of(outerContext).onion, messageIndex: index, ), - child: MessageRow()); + child: MessageRow(key: Provider.of(outerContext).getMessageKey(index))); }, ), ))); diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 8c918e9..f927968 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -9,6 +9,8 @@ import '../settings.dart'; import 'messagebubble.dart'; class MessageRow extends StatefulWidget { + MessageRow({Key key}): super(key: key); + @override _MessageRowState createState() => _MessageRowState(); } -- 2.25.1 From 70dc9d690d9b63456c91c3ea7ef16cf81ed961af Mon Sep 17 00:00:00 2001 From: erinn Date: Mon, 3 May 2021 11:47:54 -0700 Subject: [PATCH 3/3] support for indexed acks, better group sendmessage handling and group acks, clean up warnings --- lib/cwtch/cwtchNotifier.dart | 12 +++--- lib/cwtch/ffi.dart | 1 + lib/cwtch/gomobile.dart | 1 + lib/main_test.dart | 4 +- lib/model.dart | 39 ++++++++++++++----- lib/views/contactsview.dart | 1 - lib/views/groupsettingsview.dart | 13 ++++++- lib/views/messageview.dart | 5 +-- lib/views/profilemgrview.dart | 1 - lib/views/torstatusview.dart | 3 -- lib/widgets/messagebubble.dart | 65 +++++++++++++++++--------------- lib/widgets/messagerow.dart | 4 +- test/profileimage_test.dart | 1 - 13 files changed, 89 insertions(+), 61 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index e25c239..88003e1 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,9 +1,7 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:provider/provider.dart'; import 'package:flutter_app/torstatus.dart'; -import 'package:flutter_app/widgets/messagebubble.dart'; import '../errorHandler.dart'; import '../model.dart'; @@ -72,9 +70,13 @@ class CwtchNotifier { Provider.of(key.currentContext, listen: false).ackd = true; break; case "NewMessageFromGroup": - profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).unreadMessages++; - profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).totalMessages++; - profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["GroupID"], DateTime.now()); + if (data["ProfileOnion"] != data["RemotePeer"]) {//not from me + profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).unreadMessages++; + profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).totalMessages++; + profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["GroupID"], DateTime.now()); + } else {// from me (already displayed - do not update counter) + //todo: update ack - once group messages + } break; case "AppError": print("New App Error: $data"); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 2f747d2..8c16594 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -305,6 +305,7 @@ class CwtchFfi implements Cwtch { // ignore: non_constant_identifier_names void ResetTor() { var resetTor = library.lookup>("c_ResetTor"); + // ignore: non_constant_identifier_names final ResetTor = resetTor.asFunction(); ResetTor(); } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index ae329c3..5c4de7b 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -156,6 +156,7 @@ class CwtchGomobile implements Cwtch { } @override + // ignore: non_constant_identifier_names void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) { cwtchPlatform.invokeMethod("SetGroupAttribute", {"ProfileOnion": profileOnion, "groupHandle": groupHandle, "key": key, "value": value}); } diff --git a/lib/main_test.dart b/lib/main_test.dart index 7ed40f0..7a70ffe 100644 --- a/lib/main_test.dart +++ b/lib/main_test.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -13,7 +12,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:glob/glob.dart'; import 'package:glob/list_local_fs.dart'; @@ -43,7 +41,7 @@ class DiskAssetBundle extends CachingAssetBundle { for (final pattern in globs) { await for (final path in Glob(pattern).list(root: from)) { if (path is File) { - final bytes = await (path as File).readAsBytes() as Uint8List; + final bytes = await (path as File).readAsBytes()/* as Uint8List*/; cache[path.path] = ByteData.view(bytes.buffer); } } diff --git a/lib/model.dart b/lib/model.dart index 6a5fa49..a4fb3d1 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -251,7 +251,7 @@ class ContactInfoState extends ChangeNotifier { savePeerHistory = "DeleteHistoryConfirmed", numMessages = 0, numUnread = 0, - lastMessageTime = null, + lastMessageTime, server = "", }) { this._nickname = nickname; @@ -355,7 +355,9 @@ class MessageState extends ChangeNotifier { String _message; DateTime _timestamp; String _senderOnion; + String _senderImage; bool _ackd = false; + bool _loaded = false; MessageState({ BuildContext context, @@ -363,26 +365,43 @@ class MessageState extends ChangeNotifier { this.contactHandle, this.messageIndex, }) { - Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { - dynamic messageWrapper = jsonDecode(jsonMessage); - dynamic message = jsonDecode(messageWrapper['Message']); - this._message = message['d']; - this._timestamp = DateTime.tryParse(messageWrapper['Timestamp']); - this._senderOnion = messageWrapper['PeerID']; - //update ackd last as it's changenotified - this._ackd = messageWrapper['Acknowledged']; - }); + tryLoad(context); } get message => this._message; get timestamp => this._timestamp; get ackd => this._ackd; get senderOnion => this._senderOnion; + get senderImage => this._senderImage; + get loaded => this._loaded; set ackd(bool newVal) { this._ackd = newVal; notifyListeners(); } + + void tryLoad(BuildContext context) { + Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { + dynamic messageWrapper = jsonDecode(jsonMessage); + if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { + this._senderOnion = profileOnion; + //todo: remove once sent group messages are prestored + Future.delayed(const Duration(milliseconds: 200), () { + tryLoad(context); + }); + return; + } + + dynamic message = jsonDecode(messageWrapper['Message']); + this._message = message['d']; + this._timestamp = DateTime.tryParse(messageWrapper['Timestamp']); + this._senderOnion = messageWrapper['PeerID']; + this._senderImage = messageWrapper['ContactImage']; + this._loaded = true; + //update ackd last as it's changenotified + this._ackd = messageWrapper['Acknowledged']; + }); + } } ///////////// diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 2ccbaad..cd80a1a 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_app/widgets/contactrow.dart'; import 'package:provider/provider.dart'; -import '../main.dart'; import 'addcontactview.dart'; import '../model.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 6fb3c30..c614115 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter_app/model.dart'; import 'package:flutter_app/widgets/buttontextfield.dart'; @@ -72,6 +71,18 @@ class _GroupSettingsViewState extends State { controller: TextEditingController(text: Provider.of(context, listen: false).onion), ) ]), + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context).server), + SizedBox( + height: 20, + ), + CwtchTextField( + controller: TextEditingController(text: Provider.of(context, listen: false).server), + ) + ]), // Nickname Save Button Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 72c6031..f72104e 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -6,7 +6,6 @@ import 'package:provider/provider.dart'; import '../main.dart'; import '../model.dart'; -import '../opaque.dart'; import '../settings.dart'; import '../widgets/messagelist.dart'; import 'groupsettingsview.dart'; @@ -84,9 +83,9 @@ class _MessageViewState extends State { .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); ctrlrCompose.clear(); focusNode.requestFocus(); - if (Provider.of(context, listen: false).isGroup == false) { + Future.delayed(const Duration(milliseconds: 80), () { Provider.of(context, listen: false).totalMessages++; - } + }); } Widget _buildComposeBox() { diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index e9d1c74..77535da 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_app/settings.dart'; -import 'package:flutter_app/torstatus.dart'; import 'package:flutter_app/views/torstatusview.dart'; import 'package:flutter_app/widgets/passwordfield.dart'; import 'package:flutter_app/widgets/tor_icon.dart'; diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index 63602f0..bac6c5f 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter_app/settings.dart'; import 'package:flutter_app/torstatus.dart'; import 'package:flutter_app/widgets/tor_icon.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 3f80ca1..2bc67dc 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -24,6 +24,40 @@ class MessageBubbleState extends State { // user-configurable timestamps prolly ideal? #todo prettyDate = DateFormat.yMd().add_jm().format(Provider.of(context).timestamp); } + + var wdgSender = Text(Provider.of(context).senderOnion ?? "", + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); + + var wdgMessage = SelectableText( + (Provider.of(context).message ?? "") + '\u202F', + key: Key(myKey), + focusNode: _focus, + style: TextStyle( + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + ); + + var wdgDecorations = Center( + widthFactor: 1.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(prettyDate, + style: TextStyle( + fontSize: 9.0, + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: fromMe ? TextAlign.right : TextAlign.left), + !fromMe + ? SizedBox(width:1,height:1) + : Provider.of(context).ackd + ? Icon(Icons.check_circle_outline, color: Provider.of(context).theme.messageFromMeTextColor(), size: 12) + : Icon(Icons.hourglass_bottom_outlined, color: Provider.of(context).theme.messageFromMeTextColor(), size: 12) + ], + )); + return LayoutBuilder(builder: (context, constraints) { //print(constraints.toString()+", "+constraints.maxWidth.toString()); return Container( @@ -45,36 +79,7 @@ class MessageBubbleState extends State { crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [ - //Flexible( - //fit: BoxFit.contain, - SelectableText( - (Provider.of(context).message ?? "") + '\u202F', - key: Key(myKey), - focusNode: _focus, - style: TextStyle( - color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), - ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ), - Center( - widthFactor: 1.0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(prettyDate, - style: TextStyle( - fontSize: 9.0, - color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), - ), - textAlign: fromMe ? TextAlign.right : TextAlign.left), - Provider.of(context).ackd - ? Icon(Icons.check_circle_outline, color: Provider.of(context).theme.messageFromMeTextColor(), size: 12) - : Icon(Icons.hourglass_bottom_outlined, color: Provider.of(context).theme.messageFromMeTextColor(), size: 12) - ], - )) - ])))); + children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))); }); } } diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index f927968..96242f7 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_app/views/messageview.dart'; import 'package:flutter_app/widgets/profileimage.dart'; import 'package:provider/provider.dart'; -import '../main.dart'; import '../model.dart'; import '../settings.dart'; import 'messagebubble.dart'; @@ -35,7 +33,7 @@ class _MessageRowState extends State { var contact = Provider.of(context); Widget wdgPortrait = ProfileImage( diameter: 48.0, - imagePath: contact.imagePath, + imagePath: Provider.of(context).senderImage ?? contact.imagePath, //maskOut: contact.status != "Authenticated", border: contact.status == "Authenticated" ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()); diff --git a/test/profileimage_test.dart b/test/profileimage_test.dart index bea67be..965d6c2 100644 --- a/test/profileimage_test.dart +++ b/test/profileimage_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_app/opaque.dart'; import 'package:flutter_app/settings.dart'; -import 'package:flutter_app/widgets/cwtchlabel.dart'; import 'package:flutter_app/widgets/profileimage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; -- 2.25.1