Merge pull request 'acknowledgements' (#66) from messagefixes into trunk
continuous-integration/drone/push Build is failing Details

Reviewed-on: #66
This commit is contained in:
Sarah Jamie Lewis 2021-05-03 11:52:57 -07:00
commit 4b5958644c
14 changed files with 113 additions and 63 deletions

View File

@ -1,5 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'package:provider/provider.dart';
import 'package:flutter_app/torstatus.dart'; import 'package:flutter_app/torstatus.dart';
@ -62,10 +62,21 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).totalMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).totalMessages++;
profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now()); profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
break; 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<MessageState>(key.currentContext, listen: false).ackd = true;
break;
case "NewMessageFromGroup": case "NewMessageFromGroup":
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).unreadMessages++; if (data["ProfileOnion"] != data["RemotePeer"]) {//not from me
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).totalMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).unreadMessages++;
profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["GroupID"], DateTime.now()); 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; break;
case "AppError": case "AppError":
print("New App Error: $data"); print("New App Error: $data");

View File

@ -305,6 +305,7 @@ class CwtchFfi implements Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ResetTor() { void ResetTor() {
var resetTor = library.lookup<NativeFunction<Void Function()>>("c_ResetTor"); var resetTor = library.lookup<NativeFunction<Void Function()>>("c_ResetTor");
// ignore: non_constant_identifier_names
final ResetTor = resetTor.asFunction<void Function()>(); final ResetTor = resetTor.asFunction<void Function()>();
ResetTor(); ResetTor();
} }

View File

@ -156,6 +156,7 @@ class CwtchGomobile implements Cwtch {
} }
@override @override
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) { void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) {
cwtchPlatform.invokeMethod("SetGroupAttribute", {"ProfileOnion": profileOnion, "groupHandle": groupHandle, "key": key, "value": value}); cwtchPlatform.invokeMethod("SetGroupAttribute", {"ProfileOnion": profileOnion, "groupHandle": groupHandle, "key": key, "value": value});
} }

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -13,7 +12,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart'; import 'package:glob/list_local_fs.dart';
@ -43,7 +41,7 @@ class DiskAssetBundle extends CachingAssetBundle {
for (final pattern in globs) { for (final pattern in globs) {
await for (final path in Glob(pattern).list(root: from)) { await for (final path in Glob(pattern).list(root: from)) {
if (path is File) { 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); cache[path.path] = ByteData.view(bytes.buffer);
} }
} }

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_app/models/servers.dart'; import 'package:flutter_app/models/servers.dart';
import 'package:flutter_app/widgets/messagebubble.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
@ -232,6 +233,7 @@ class ContactInfoState extends ChangeNotifier {
int _unreadMessages = 0; int _unreadMessages = 0;
int _totalMessages = 0; int _totalMessages = 0;
DateTime _lastMessageTime; DateTime _lastMessageTime;
Map<int, GlobalKey> keys;
// todo: a nicer way to model contacts, groups and other "entities" // todo: a nicer way to model contacts, groups and other "entities"
bool _isGroup; bool _isGroup;
@ -249,7 +251,7 @@ class ContactInfoState extends ChangeNotifier {
savePeerHistory = "DeleteHistoryConfirmed", savePeerHistory = "DeleteHistoryConfirmed",
numMessages = 0, numMessages = 0,
numUnread = 0, numUnread = 0,
lastMessageTime = null, lastMessageTime,
server = "", server = "",
}) { }) {
this._nickname = nickname; this._nickname = nickname;
@ -263,6 +265,7 @@ class ContactInfoState extends ChangeNotifier {
this._savePeerHistory = savePeerHistory; this._savePeerHistory = savePeerHistory;
this._lastMessageTime = lastMessageTime; this._lastMessageTime = lastMessageTime;
this._server = server; this._server = server;
keys = Map<int, GlobalKey>();
} }
get nickname => this._nickname; get nickname => this._nickname;
@ -336,6 +339,13 @@ class ContactInfoState extends ChangeNotifier {
return this.status == "Authenticated"; return this.status == "Authenticated";
} }
} }
GlobalKey<MessageBubbleState> getMessageKey(int index) {
if (keys[index] == null) {
keys[index] = GlobalKey<MessageBubbleState>();
}
return keys[index];
}
} }
class MessageState extends ChangeNotifier { class MessageState extends ChangeNotifier {
@ -345,7 +355,9 @@ class MessageState extends ChangeNotifier {
String _message; String _message;
DateTime _timestamp; DateTime _timestamp;
String _senderOnion; String _senderOnion;
String _senderImage;
bool _ackd = false; bool _ackd = false;
bool _loaded = false;
MessageState({ MessageState({
BuildContext context, BuildContext context,
@ -353,26 +365,43 @@ class MessageState extends ChangeNotifier {
this.contactHandle, this.contactHandle,
this.messageIndex, this.messageIndex,
}) { }) {
Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { tryLoad(context);
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'];
});
} }
get message => this._message; get message => this._message;
get timestamp => this._timestamp; get timestamp => this._timestamp;
get ackd => this._ackd; get ackd => this._ackd;
get senderOnion => this._senderOnion; get senderOnion => this._senderOnion;
get senderImage => this._senderImage;
get loaded => this._loaded;
set ackd(bool newVal) { set ackd(bool newVal) {
this._ackd = newVal; this._ackd = newVal;
notifyListeners(); notifyListeners();
} }
void tryLoad(BuildContext context) {
Provider.of<FlwtchState>(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'];
});
}
} }
///////////// /////////////

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app/widgets/contactrow.dart'; import 'package:flutter_app/widgets/contactrow.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../main.dart';
import 'addcontactview.dart'; import 'addcontactview.dart';
import '../model.dart'; import '../model.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_app/model.dart'; import 'package:flutter_app/model.dart';
import 'package:flutter_app/widgets/buttontextfield.dart'; import 'package:flutter_app/widgets/buttontextfield.dart';
@ -72,6 +71,18 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion), controller: TextEditingController(text: Provider.of<ContactInfoState>(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<ContactInfoState>(context, listen: false).server),
)
]),
// Nickname Save Button // Nickname Save Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox( SizedBox(

View File

@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../opaque.dart';
import '../settings.dart'; import '../settings.dart';
import '../widgets/messagelist.dart'; import '../widgets/messagelist.dart';
import 'groupsettingsview.dart'; import 'groupsettingsview.dart';
@ -84,9 +83,9 @@ class _MessageViewState extends State<MessageView> {
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm)); .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
ctrlrCompose.clear(); ctrlrCompose.clear();
focusNode.requestFocus(); focusNode.requestFocus();
if (Provider.of<ContactInfoState>(context, listen: false).isGroup == false) { Future.delayed(const Duration(milliseconds: 80), () {
Provider.of<ContactInfoState>(context, listen: false).totalMessages++; Provider.of<ContactInfoState>(context, listen: false).totalMessages++;
} });
} }
Widget _buildComposeBox() { Widget _buildComposeBox() {

View File

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app/settings.dart'; import 'package:flutter_app/settings.dart';
import 'package:flutter_app/torstatus.dart';
import 'package:flutter_app/views/torstatusview.dart'; import 'package:flutter_app/views/torstatusview.dart';
import 'package:flutter_app/widgets/passwordfield.dart'; import 'package:flutter_app/widgets/passwordfield.dart';
import 'package:flutter_app/widgets/tor_icon.dart'; import 'package:flutter_app/widgets/tor_icon.dart';

View File

@ -1,7 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_app/torstatus.dart'; import 'package:flutter_app/torstatus.dart';
import 'package:flutter_app/widgets/tor_icon.dart'; import 'package:flutter_app/widgets/tor_icon.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@ -7,10 +7,10 @@ import '../settings.dart';
class MessageBubble extends StatefulWidget { class MessageBubble extends StatefulWidget {
@override @override
_MessageBubbleState createState() => _MessageBubbleState(); MessageBubbleState createState() => MessageBubbleState();
} }
class _MessageBubbleState extends State<MessageBubble> { class MessageBubbleState extends State<MessageBubble> {
FocusNode _focus = FocusNode(); FocusNode _focus = FocusNode();
@override @override
@ -24,6 +24,40 @@ class _MessageBubbleState extends State<MessageBubble> {
// user-configurable timestamps prolly ideal? #todo // user-configurable timestamps prolly ideal? #todo
prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp); prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp);
} }
var wdgSender = Text(Provider.of<MessageState>(context).senderOnion ?? "",
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText(
(Provider.of<MessageState>(context).message ?? "") + '\u202F',
key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(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<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: fromMe ? TextAlign.right : TextAlign.left),
!fromMe
? SizedBox(width:1,height:1)
: Provider.of<MessageState>(context).ackd
? Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
],
));
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString()); //print(constraints.toString()+", "+constraints.maxWidth.toString());
return Container( return Container(
@ -45,36 +79,7 @@ class _MessageBubbleState extends State<MessageBubble> {
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: [ children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))));
//Flexible(
//fit: BoxFit.contain,
SelectableText(
(Provider.of<MessageState>(context).message ?? "") + '\u202F',
key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(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<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: fromMe ? TextAlign.right : TextAlign.left),
Provider.of<MessageState>(context).ackd
? Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
],
))
]))));
}); });
} }
} }

View File

@ -26,6 +26,7 @@ class _MessageListState extends State<MessageList> {
if (force || Provider.of<ContactInfoState>(context, listen: false).totalMessages > lastMessageCount) { if (force || Provider.of<ContactInfoState>(context, listen: false).totalMessages > lastMessageCount) {
if (ctrlr1.position.pixels == lastMaxScroll) { if (ctrlr1.position.pixels == lastMaxScroll) {
ctrlr1.jumpTo(ctrlr1.position.maxScrollExtent); ctrlr1.jumpTo(ctrlr1.position.maxScrollExtent);
Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0;
} }
setState(() { setState(() {
@ -63,7 +64,7 @@ class _MessageListState extends State<MessageList> {
contactHandle: Provider.of<ContactInfoState>(outerContext).onion, contactHandle: Provider.of<ContactInfoState>(outerContext).onion,
messageIndex: index, messageIndex: index,
), ),
child: MessageRow()); child: MessageRow(key: Provider.of<ContactInfoState>(outerContext).getMessageKey(index)));
}, },
), ),
))); )));

View File

@ -1,14 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app/views/messageview.dart';
import 'package:flutter_app/widgets/profileimage.dart'; import 'package:flutter_app/widgets/profileimage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubble.dart'; import 'messagebubble.dart';
class MessageRow extends StatefulWidget { class MessageRow extends StatefulWidget {
MessageRow({Key key}): super(key: key);
@override @override
_MessageRowState createState() => _MessageRowState(); _MessageRowState createState() => _MessageRowState();
} }
@ -33,7 +33,7 @@ class _MessageRowState extends State<MessageRow> {
var contact = Provider.of<ContactInfoState>(context); var contact = Provider.of<ContactInfoState>(context);
Widget wdgPortrait = ProfileImage( Widget wdgPortrait = ProfileImage(
diameter: 48.0, diameter: 48.0,
imagePath: contact.imagePath, imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated", //maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()); border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor());

View File

@ -8,7 +8,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app/opaque.dart'; import 'package:flutter_app/opaque.dart';
import 'package:flutter_app/settings.dart'; import 'package:flutter_app/settings.dart';
import 'package:flutter_app/widgets/cwtchlabel.dart';
import 'package:flutter_app/widgets/profileimage.dart'; import 'package:flutter_app/widgets/profileimage.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';