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 @@
+
+
+
+
\ 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;
}