diff --git a/assets/core/broken_heart_24.png b/assets/core/broken_heart_24.png new file mode 100644 index 0000000..9ad024f Binary files /dev/null and b/assets/core/broken_heart_24.png differ diff --git a/assets/core/broken_heart_24.svg b/assets/core/broken_heart_24.svg new file mode 100644 index 0000000..73f47f7 --- /dev/null +++ b/assets/core/broken_heart_24.svg @@ -0,0 +1,71 @@ + + + +image/svg+xml + + + + + + + \ No newline at end of file diff --git a/lib/model.dart b/lib/model.dart index bead765..cb4fd9e 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -363,6 +363,7 @@ class MessageState extends ChangeNotifier { bool _ackd = false; bool _error = false; bool _loaded = false; + bool _malformed = false; MessageState({ BuildContext context, @@ -379,6 +380,7 @@ class MessageState extends ChangeNotifier { get timestamp => this._timestamp; get ackd => this._ackd; get error => this._error; + get malformed => this._malformed; get senderOnion => this._senderOnion; get senderImage => this._senderImage; get loaded => this._loaded; @@ -399,50 +401,54 @@ class MessageState extends ChangeNotifier { 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'] == '{}') { - //todo: remove once sent group messages are prestored - Future.delayed(const Duration(milliseconds: 2), () { - tryLoad(context); - }); - return; - } + try { + 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: 2), () { + tryLoad(context); + }); + return; + } + dynamic message = jsonDecode(messageWrapper['Message']); + this._message = message['d']; + this._overlay = int.parse(message['o'].toString()); + this._timestamp = DateTime.tryParse(messageWrapper['Timestamp']); + this._senderOnion = messageWrapper['PeerID']; + this._senderImage = messageWrapper['ContactImage']; - dynamic message = jsonDecode(messageWrapper['Message']); - this._message = message['d']; - this._overlay = int.parse(message['o'].toString()); - this._timestamp = DateTime.tryParse(messageWrapper['Timestamp']); - this._senderOnion = messageWrapper['PeerID']; - this._senderImage = messageWrapper['ContactImage']; + // If this is a group, store the signature + if (contactHandle.length == 32) { + this._signature = messageWrapper['Signature']; + } - // If this is a group, store the signature - if (contactHandle.length == 32) { - this._signature = messageWrapper['Signature']; - } - - // if this is an invite, get the contact handle - if (this.isInvite) { - if (message['d'].toString().length == 56) { - this._inviteTarget = message['d']; - var targetContact = Provider.of(context).contactList.getContact(this._inviteTarget); - this._inviteNick = targetContact == null ? message['d'] : targetContact.nickname; - } else { - var parts = message['d'].toString().split("||"); - if (parts.length == 2) { - print("jsondecoding: "+utf8.fuse(base64).decode(parts[1].substring(5))); - var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); - this._inviteTarget = jsonObj['GroupID']; - this._inviteNick = jsonObj['GroupName']; + // if this is an invite, get the contact handle + if (this.isInvite) { + if (message['d'].toString().length == 56) { + this._inviteTarget = message['d']; + var targetContact = Provider.of(context).contactList.getContact(this._inviteTarget); + this._inviteNick = targetContact == null ? message['d'] : targetContact.nickname; + } else { + var parts = message['d'].toString().split("||"); + if (parts.length == 2) { + print("jsondecoding: " + utf8.fuse(base64).decode(parts[1].substring(5))); + var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); + this._inviteTarget = jsonObj['GroupID']; + this._inviteNick = jsonObj['GroupName']; + } } } - } - this._loaded = true; + this._loaded = true; - //update ackd and error last as they are changenotified - this.ackd = messageWrapper['Acknowledged']; - if (messageWrapper['Error'] != null) { - this.error = true; + //update ackd and error last as they are changenotified + this.ackd = messageWrapper['Acknowledged']; + if (messageWrapper['Error'] != null) { + this.error = true; + } + } catch (e) { + this._malformed = true; } }); } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index f163348..090a52d 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -81,18 +81,16 @@ class _MessageViewState extends State { void _sendMessage([String ignoredParam]) { ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); - Provider.of(context, listen: false).cwtch.SendMessage( - Provider.of(context, listen: false).profileOnion, - Provider.of(context, listen: false).onion, - jsonEncode(cm)); + Provider.of(context, listen: false) + .cwtch + .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); _sendMessageHelper(); } void _sendInvitation([String ignoredParam]) { - Provider.of(context, listen: false).cwtch.SendInvitation( - Provider.of(context, listen: false).profileOnion, - Provider.of(context, listen: false).onion, - this.selectedContact); + Provider.of(context, listen: false) + .cwtch + .SendInvitation(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, this.selectedContact); _sendMessageHelper(); } @@ -121,23 +119,25 @@ class _MessageViewState extends State { textInputAction: TextInputAction.send, onSubmitted: _sendMessage, )), - Column(children:[SizedBox( - width: 100, - height: 50, - child: Padding( - padding: EdgeInsets.fromLTRB(2, 2, 2, 2), - child: ElevatedButton( - child: Icon(Icons.send, size: 24, color: Provider.of(context).theme.mainTextColor()), - style: ButtonStyle( - fixedSize: MaterialStateProperty.all(Size(86, 50)), - backgroundColor: MaterialStateProperty.all(Provider.of(context).theme.defaultButtonColor()), - ), - onPressed: _sendMessage, - ))), - SizedBox( - width: 86, height: 40, - child: IconButton(icon: Icon(Icons.insert_invitation, size: 12, color: Provider.of(context).theme.mainTextColor()), onPressed: () => _modalSendInvitation(context)) - ),]) + Column(children: [ + SizedBox( + width: 100, + height: 50, + child: Padding( + padding: EdgeInsets.fromLTRB(2, 2, 2, 2), + child: ElevatedButton( + child: Icon(Icons.send, size: 24, color: Provider.of(context).theme.mainTextColor()), + style: ButtonStyle( + fixedSize: MaterialStateProperty.all(Size(86, 50)), + backgroundColor: MaterialStateProperty.all(Provider.of(context).theme.defaultButtonColor()), + ), + onPressed: _sendMessage, + ))), + SizedBox( + width: 86, + height: 40, + child: IconButton(icon: Icon(Icons.insert_invitation, size: 12, color: Provider.of(context).theme.mainTextColor()), onPressed: () => _modalSendInvitation(context))), + ]) ], ), ); @@ -165,10 +165,16 @@ class _MessageViewState extends State { SizedBox( height: 20, ), - ChangeNotifierProvider.value(value: Provider.of(ctx, listen: false), child: DropdownContacts(onChanged: (newVal) { - setState((){ this.selectedContact = newVal; }); - })), - SizedBox(height: 20,), + ChangeNotifierProvider.value( + value: Provider.of(ctx, listen: false), + child: DropdownContacts(onChanged: (newVal) { + setState(() { + this.selectedContact = newVal; + }); + })), + SizedBox( + height: 20, + ), ElevatedButton( child: Text(AppLocalizations.of(bcontext).inviteBtn, semanticsLabel: AppLocalizations.of(bcontext).inviteBtn), onPressed: () { diff --git a/lib/widgets/DropdownContacts.dart b/lib/widgets/DropdownContacts.dart index 9ea288e..5a8d80a 100644 --- a/lib/widgets/DropdownContacts.dart +++ b/lib/widgets/DropdownContacts.dart @@ -8,7 +8,9 @@ import '../model.dart'; // Displays nicknames to UI but uses handles as values // Pass an onChanged handler to access value class DropdownContacts extends StatefulWidget { - DropdownContacts({this.onChanged,}); + DropdownContacts({ + this.onChanged, + }); final Function(dynamic) onChanged; @override @@ -20,15 +22,18 @@ class _DropdownContactsState extends State { @override Widget build(BuildContext context) { - return DropdownButton(value: this.selected, items: Provider.of(context, listen: false).contactList.contacts.map>((ContactInfoState contact) { - return DropdownMenuItem(value: contact.onion, child: Text(contact.nickname??contact.onion)); - }).toList(), onChanged: (newVal) { - setState(() { - this.selected = newVal; - }); - if (widget.onChanged != null) { - widget.onChanged(newVal); - } - }); + return DropdownButton( + value: this.selected, + items: Provider.of(context, listen: false).contactList.contacts.map>((ContactInfoState contact) { + return DropdownMenuItem(value: contact.onion, child: Text(contact.nickname ?? contact.onion)); + }).toList(), + onChanged: (newVal) { + setState(() { + this.selected = newVal; + }); + if (widget.onChanged != null) { + widget.onChanged(newVal); + } + }); } } diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 32fcc41..8f52d0b 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -43,27 +43,33 @@ class InvitationBubbleState extends State { senderDisplayStr = contact.nickname ?? contact.onion; } } - var wdgSender = Center(widthFactor:1, child: SelectableText(senderDisplayStr + '\u202F', - style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor()))); + var wdgSender = Center( + widthFactor: 1, + child: SelectableText(senderDisplayStr + '\u202F', + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor()))); // todo: translations var messageStr = ""; if (fromMe) { //todo: get group name? - messageStr = "You sent an invitation for "+(isGroup ? "a group" : Provider.of(context).message ?? ""); + messageStr = "You sent an invitation for " + (isGroup ? "a group" : Provider.of(context).message ?? ""); } else { - messageStr = (isGroup ? "You have been invited to join "+(Provider.of(context).inviteNick??"") : "This is a contact suggestion for:") + "\n" + (Provider.of(context).inviteTarget ?? ""); + messageStr = (isGroup ? "You have been invited to join " + (Provider.of(context).inviteNick ?? "") : "This is a contact suggestion for:") + + "\n" + + (Provider.of(context).inviteTarget ?? ""); } - var wdgMessage = Center(widthFactor:1, child: SelectableText( - messageStr + '\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 wdgMessage = Center( + widthFactor: 1, + child: SelectableText( + messageStr + '\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, + )); Widget wdgDecorations; if (fromMe) { @@ -72,15 +78,14 @@ class InvitationBubbleState extends State { 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), + 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) + ? 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) ], )); } else if (isAccepted) { @@ -88,10 +93,12 @@ class InvitationBubbleState extends State { } else if (this.rejected) { wdgDecorations = Text("Rejected."); } else { - wdgDecorations = Center(widthFactor:1,child:Row(children: [ - Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Reject"), onPressed: _btnReject)), - Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Accept"), onPressed: _btnAccept)), - ])); + wdgDecorations = Center( + widthFactor: 1, + child: Row(children: [ + Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Reject"), onPressed: _btnReject)), + Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Accept"), onPressed: _btnAccept)), + ])); } return LayoutBuilder(builder: (context, constraints) { @@ -110,21 +117,26 @@ class InvitationBubbleState extends State { bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), ), ), - child: Center(widthFactor: 1.0,child:Padding( - padding: EdgeInsets.all(9.0), - child: Row(mainAxisSize: MainAxisSize.min, children: [Center(widthFactor: 1,child: Padding(padding:EdgeInsets.all(4), child:Icon(Icons.group_add, size: 32))), - Center(widthFactor: 1.0,child: Column( - crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])), - ]))))); + child: Center( + widthFactor: 1.0, + child: Padding( + padding: EdgeInsets.all(9.0), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(4), child: Icon(Icons.group_add, size: 32))), + Center( + widthFactor: 1.0, + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])), + ]))))); }); } void _btnReject() { //todo: how should we track inline invite rejections? - setState(()=>this.rejected = true); + setState(() => this.rejected = true); } void _btnAccept() { diff --git a/lib/widgets/malformedbubble.dart b/lib/widgets/malformedbubble.dart new file mode 100644 index 0000000..cc8bfcd --- /dev/null +++ b/lib/widgets/malformedbubble.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; +import 'dart:ffi'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../settings.dart'; + +final Color malformedColor = Color(0xFFE85DA1); + +// MalformedBubble is displayed in the case of a malformed message +class MalformedBubble extends StatefulWidget { + @override + MalformedBubbleState createState() => MalformedBubbleState(); +} + +class MalformedBubbleState extends State { + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return Center( + widthFactor: 1.0, + child: Container( + decoration: BoxDecoration( + color: malformedColor, + border: Border.all(color: malformedColor, width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.zero, + topRight: Radius.zero, + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ), + ), + child: Center( + widthFactor: 1.0, + child: Padding( + padding: EdgeInsets.all(9.0), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Center( + widthFactor: 1, + child: Padding( + padding: EdgeInsets.all(4), + child: Image( + image: AssetImage("assets/core/broken_heart_24.png"), + filterQuality: FilterQuality.medium, + // We need some theme specific blending here...we might want to consider making this a theme level attribute + colorBlendMode: BlendMode.srcIn, + color: Provider.of(context).theme.mainTextColor(), + isAntiAlias: false, + width: 32, + height: 32))), + Center( + widthFactor: 1.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [Text("Malformed Message")], + )) + ]))))); + }); + } +} diff --git a/lib/widgets/messageloadingbubble.dart b/lib/widgets/messageloadingbubble.dart index 237c770..f9605d1 100644 --- a/lib/widgets/messageloadingbubble.dart +++ b/lib/widgets/messageloadingbubble.dart @@ -13,6 +13,6 @@ class MessageLoadingBubble extends StatefulWidget { class MessageLoadingBubbleState extends State { @override Widget build(BuildContext context) { - return Center(child:Row(children:[SizedBox(width:40, height:100, child: Text(""))])); + return Center(child: Row(children: [SizedBox(width: 40, height: 100, child: Text(""))])); } } diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 25385ee..035b068 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -9,6 +9,7 @@ import '../main.dart'; import '../model.dart'; import '../settings.dart'; import 'invitationbubble.dart'; +import 'malformedbubble.dart'; import 'messagebubble.dart'; import 'messageloadingbubble.dart'; @@ -23,8 +24,17 @@ class _MessageRowState extends State { @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; + var malformed = Provider.of(context).malformed; - Widget wdgBubble = Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of(context).loaded == true ? widgetForOverlay(Provider.of(context).overlay) : MessageLoadingBubble()); + // If the message is malformed then override fromme as we can't trust it + if (malformed) { + fromMe = false; + } + + Widget wdgBubble = Flexible( + flex: 3, + fit: FlexFit.loose, + child: malformed ? MalformedBubble() : (Provider.of(context).loaded == true ? widgetForOverlay(Provider.of(context).overlay) : MessageLoadingBubble())); Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of(context).theme.dropShadowColor()); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); var widgetRow = []; @@ -38,12 +48,14 @@ class _MessageRowState extends State { } else { var contact = Provider.of(context); Widget wdgPortrait = GestureDetector( - onTap: _btnAdd, - child: ProfileImage( - diameter: 48.0, - 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())); + onTap: _btnAdd, + child: Padding( + padding: EdgeInsets.all(4.0), + child: ProfileImage( + diameter: 48.0, + 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()))); widgetRow = [ wdgPortrait, @@ -58,9 +70,11 @@ class _MessageRowState extends State { Widget widgetForOverlay(int o) { switch (o) { - case 1: return MessageBubble(); + case 1: + return MessageBubble(); case 100: - case 101: return InvitationBubble(); + case 101: + return InvitationBubble(); } return null; }