From ddfc7fc43c76928cd38684d2969b2da4d4f3d13d Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 6 Jul 2021 12:46:39 -0700 Subject: [PATCH] Refactor Message/MessageState to make adding more Message Types simpler --- lib/cwtch/cwtchNotifier.dart | 9 +- lib/model.dart | 137 +-------------- lib/models/message.dart | 100 +++++++++++ lib/models/messages/invitemessage.dart | 78 +++++++++ lib/models/messages/malformedmessage.dart | 33 ++++ lib/models/messages/quotedmessage.dart | 94 ++++++++++ lib/models/messages/textmessage.dart | 37 ++++ lib/views/messageview.dart | 59 +++---- lib/widgets/invitationbubble.dart | 58 +++--- lib/widgets/messagebubble.dart | 30 ++-- lib/widgets/messagelist.dart | 37 ++-- lib/widgets/messagerow.dart | 77 ++++---- lib/widgets/quotedmessage.dart | 204 +++++++++------------- pubspec.lock | 4 +- 14 files changed, 563 insertions(+), 394 deletions(-) create mode 100644 lib/models/message.dart create mode 100644 lib/models/messages/invitemessage.dart create mode 100644 lib/models/messages/malformedmessage.dart create mode 100644 lib/models/messages/quotedmessage.dart create mode 100644 lib/models/messages/textmessage.dart diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 7551526a..4ecc0ec5 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/notification_manager.dart'; import 'package:provider/provider.dart'; @@ -115,7 +116,7 @@ class CwtchNotifier { var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); if (key == null) break; try { - var message = Provider.of(key.currentContext!, listen: false); + var message = Provider.of(key.currentContext!, listen: false); if (message == null) break; message.ackd = true; } catch (e) { @@ -138,7 +139,7 @@ class CwtchNotifier { var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx); if (key == null) break; try { - var message = Provider.of(key.currentContext!, listen: false); + var message = Provider.of(key.currentContext!, listen: false); if (message == null) break; message.ackd = true; } catch (e) { @@ -156,7 +157,7 @@ class CwtchNotifier { var idx = data["Index"]; var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); try { - var message = Provider.of(key!.currentContext!, listen: false); + var message = Provider.of(key!.currentContext!, listen: false); message.error = true; } catch (e) { // ignore, we likely have an old key that has been replaced with an actual signature @@ -169,7 +170,7 @@ class CwtchNotifier { var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx); if (key == null) break; try { - var message = Provider.of(key.currentContext!, listen: false); + var message = Provider.of(key.currentContext!, listen: false); if (message == null) break; message.error = true; } catch (e) { diff --git a/lib/model.dart b/lib/model.dart index 667264c7..8830c92b 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/cupertino.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/widgets/messagebubble.dart'; @@ -343,7 +344,7 @@ class ContactInfoState extends ChangeNotifier { late int _unreadMessages = 0; late int _totalMessages = 0; late DateTime _lastMessageTime; - late Map> keys; + late Map> keys; // todo: a nicer way to model contacts, groups and other "entities" late bool _isGroup; @@ -375,7 +376,7 @@ class ContactInfoState extends ChangeNotifier { this._savePeerHistory = savePeerHistory; this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._server = server; - keys = Map>(); + keys = Map>(); } String get nickname => this._nickname; @@ -451,137 +452,11 @@ class ContactInfoState extends ChangeNotifier { } } - GlobalKey getMessageKey(String index) { + GlobalKey getMessageKey(String index) { if (keys[index] == null) { - keys[index] = GlobalKey(); + keys[index] = GlobalKey(); } - GlobalKey ret = keys[index]!; + GlobalKey ret = keys[index]!; return ret; } } - -class MessageState extends ChangeNotifier { - final String profileOnion; - final String contactHandle; - final int messageIndex; - late dynamic _message; - late int _overlay; - late String _inviteTarget; - late String _inviteNick; - late DateTime _timestamp; - late String _senderOnion; - late int _flags; - String? _senderImage; - late String _signature = ""; - late bool _ackd = false; - late bool _error = false; - late bool _loaded = false; - late bool _malformed = false; - - MessageState({ - required BuildContext context, - required this.profileOnion, - required this.contactHandle, - required this.messageIndex, - }) { - this._senderOnion = profileOnion; - tryLoad(context); - } - - get message => this._message; - get overlay => this._overlay; - get timestamp => this._timestamp; - int get flags => this._flags; - set flags(int newVal) { - this._flags = newVal; - notifyListeners(); - } - - bool get ackd => this._ackd; - bool get error => this._error; - bool get malformed => this._malformed; - bool get loaded => this._loaded; - get senderOnion => this._senderOnion; - get senderImage => this._senderImage; - get signature => this._signature; - get isInvite => this.overlay == 100 || this.overlay == 101; - get inviteTarget => this._inviteTarget; - get inviteNick => this._inviteNick; - - set ackd(bool newVal) { - this._ackd = newVal; - notifyListeners(); - } - - set error(bool newVal) { - this._error = newVal; - notifyListeners(); - } - - set malformed(bool newVal) { - this._malformed = newVal; - notifyListeners(); - } - - set loaded(bool newVal) { - // quickly-arriving messages get discarded before loading sometimes - if (!hasListeners) return; - this._loaded = newVal; - notifyListeners(); - } - - void tryLoad(BuildContext context) { - Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { - try { - dynamic messageWrapper = jsonDecode(jsonMessage); - if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { - this._senderOnion = profileOnion; - Future.delayed(const Duration(milliseconds: 2), () { - tryLoad(context); - }); - return; - } - dynamic message = jsonDecode(messageWrapper['Message']); - this._message = message['d'] as dynamic; - this._overlay = int.parse(message['o'].toString()); - this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; - this._senderOnion = messageWrapper['PeerID']; - this._senderImage = messageWrapper['ContactImage']; - this._flags = int.parse(messageWrapper['Flags'].toString(), radix: 2); - - // 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) { - var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); - this._inviteTarget = jsonObj['GroupID']; - this._inviteNick = jsonObj['GroupName']; - } - } - } - - this.loaded = true; - - //update ackd and error last as they are changenotified - this.ackd = messageWrapper['Acknowledged']; - if (messageWrapper['Error'] != null) { - this.error = true; - } - } catch (e) { - this._overlay = -1; - this.loaded = true; - this.malformed = true; - } - }); - } -} diff --git a/lib/models/message.dart b/lib/models/message.dart new file mode 100644 index 00000000..4a84f6b5 --- /dev/null +++ b/lib/models/message.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../main.dart'; +import 'messages/invitemessage.dart'; +import 'messages/malformedmessage.dart'; +import 'messages/quotedmessage.dart'; +import 'messages/textmessage.dart'; + +abstract class Message { + MessageMetadata getMetadata(); + Widget getWidget(BuildContext context); + Widget getPreviewWidget(BuildContext context); +} + +Future messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) { + try { + var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); + return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { + dynamic messageWrapper = jsonDecode(rawMessageEnvelope); + if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { + return Future.delayed(Duration(seconds: 2), () { + return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); + }); + } + + dynamic message = jsonDecode(messageWrapper['Message']); + var content = message['d'] as dynamic; + var overlay = int.parse(message['o'].toString()); + + // Construct the initial metadata + var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; + var senderHandle = messageWrapper['PeerID']; + var senderImage = messageWrapper['ContactImage']; + var flags = int.parse(messageWrapper['Flags'].toString(), radix: 2); + var ackd = messageWrapper['Acknowledged']; + var error = messageWrapper['Error'] != null; + + String? signature; + // If this is a group, store the signature + if (contactHandle.length == 32) { + signature = messageWrapper['Signature']; + } + + var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error); + + switch (overlay) { + case 1: + return TextMessage(metadata, content); + case 100: + case 101: + return InviteMessage(overlay, metadata, content); + case 10: + return QuotedMessage(metadata, content); + default: + // Metadata is valid, content is not.. + return MalformedMessage(metadata); + } + }); + } catch (e) { + return Future.value(MalformedMessage(MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true))); + } +} + +class MessageMetadata extends ChangeNotifier { + // meta-metadata + final String profileOnion; + final String contactHandle; + final int messageIndex; + + final DateTime timestamp; + final String senderHandle; + final String? senderImage; + int _flags; + bool _ackd; + bool _error; + + final String? signature; + + int get flags => this._flags; + set flags(int newVal) { + this._flags = newVal; + notifyListeners(); + } + + bool get ackd => this._ackd; + set ackd(bool newVal) { + this._ackd = newVal; + notifyListeners(); + } + + bool get error => this._error; + set error(bool newVal) { + this._error = newVal; + notifyListeners(); + } + + MessageMetadata(this.profileOnion, this.contactHandle, this.messageIndex, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); +} diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart new file mode 100644 index 00000000..5fa36dde --- /dev/null +++ b/lib/models/messages/invitemessage.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; + +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/invitationbubble.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messagebubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../model.dart'; + +class InviteMessage extends Message { + final MessageMetadata metadata; + final String content; + final int overlay; + + InviteMessage(this.overlay, this.metadata, this.content); + + @override + Widget getWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); + + String inviteTarget; + String inviteNick; + + if (this.content.length == 56) { + inviteTarget = this.content; + var targetContact = Provider.of(context).contactList.getContact(inviteTarget); + inviteNick = targetContact == null ? this.content : targetContact.nickname; + } else { + var parts = this.content.toString().split("||"); + if (parts.length == 2) { + var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); + inviteTarget = jsonObj['GroupID']; + inviteNick = jsonObj['GroupName']; + } else { + return MessageRow(MalformedBubble()); + } + } + return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick), key: Provider.of(bcontext).getMessageKey(idx)); + }); + } + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String inviteTarget; + String inviteNick; + + if (this.content.length == 56) { + inviteTarget = this.content; + var targetContact = Provider.of(context).contactList.getContact(inviteTarget); + inviteNick = targetContact == null ? this.content : targetContact.nickname; + } else { + var parts = this.content.toString().split("||"); + if (parts.length == 2) { + var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); + inviteTarget = jsonObj['GroupID']; + inviteNick = jsonObj['GroupName']; + } else { + return MalformedBubble(); + } + } + return InvitationBubble(overlay, inviteTarget, inviteNick); + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } +} diff --git a/lib/models/messages/malformedmessage.dart b/lib/models/messages/malformedmessage.dart new file mode 100644 index 00000000..dc943fe1 --- /dev/null +++ b/lib/models/messages/malformedmessage.dart @@ -0,0 +1,33 @@ +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MalformedMessage extends Message { + final MessageMetadata metadata; + MalformedMessage(this.metadata); + + @override + Widget getWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (context, child) { + return MessageRow(MalformedBubble()); + }); + } + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + return MalformedBubble(); + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } +} diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart new file mode 100644 index 00000000..b1b44254 --- /dev/null +++ b/lib/models/messages/quotedmessage.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/messages/malformedmessage.dart'; +import 'package:cwtch/widgets/messagebubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:cwtch/widgets/quotedmessage.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../main.dart'; +import '../../model.dart'; + +class LocallyIndexedMessage { + final dynamic message; + final int index; + + LocallyIndexedMessage(this.message, this.index); + + LocallyIndexedMessage.fromJson(Map json) + : message = json['Message'], + index = json['LocalIndex']; + + Map toJson() => { + 'Message': message, + 'LocalIndex': index, + }; +} + +class QuotedMessage extends Message { + final MessageMetadata metadata; + final String content; + QuotedMessage(this.metadata, this.content); + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + try { + dynamic message = jsonDecode(this.content); + return MessageBubble(message["body"]); + } catch (e) { + return MalformedMessage(this.metadata).getWidget(context); + } + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } + + @override + Widget getWidget(BuildContext context) { + try { + dynamic message = jsonDecode(this.content); + + var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]); + int messageIndex = metadata.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).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... + return null; + } + }); + + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); + return MessageRow( + QuotedMessageBubble(message["body"], quotedMessage.then((localIndex) { + if (localIndex != null) { + return messageHandler(context, metadata.profileOnion, metadata.contactHandle, localIndex.index); + } + return Future.value(MalformedMessage(this.metadata)); + })), + key: Provider.of(bcontext).getMessageKey(idx)); + }); + } catch (e) { + return MalformedMessage(this.metadata).getWidget(context); + } + } +} diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart new file mode 100644 index 00000000..716c38ae --- /dev/null +++ b/lib/models/messages/textmessage.dart @@ -0,0 +1,37 @@ +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/messagebubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../model.dart'; + +class TextMessage extends Message { + final MessageMetadata metadata; + final String content; + TextMessage(this.metadata, this.content); + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + return MessageBubble(this.content); + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } + + @override + Widget getWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); + return MessageRow(MessageBubble(this.content), key: Provider.of(bcontext).getMessageKey(idx)); + }); + } +} diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index c8d276b6..3c9abcf6 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:flutter/cupertino.dart'; @@ -108,24 +109,23 @@ class _MessageViewState extends State { if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { Provider.of(context) .cwtch - .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(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(context, listen: false) - .cwtch - .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); - } catch (e) { - - } - Provider.of(context, listen: false).selectedIndex = null; - _sendMessageHelper(); + .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(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(context, listen: false) + .cwtch + .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); + } catch (e) {} + Provider.of(context, listen: false).selectedIndex = null; + _sendMessageHelper(); }); - } else { + } else { ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); Provider.of(context, listen: false) .cwtch @@ -201,24 +201,17 @@ class _MessageViewState extends State { var children; if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { var quoted = FutureBuilder( - future: Provider.of(context) - .cwtch - .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!), + future: messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(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(context).selectedProfile - ? Provider.of(context).theme.messageFromOtherBackgroundColor() - : Provider.of(context).theme.messageFromMeBackgroundColor(), - child: Text(message["d"])); - } catch (e) { - return MalformedBubble(); - } + var message = snapshot.data! as Message; + return Container( + margin: EdgeInsets.all(5), + padding: EdgeInsets.all(5), + color: message.getMetadata().senderHandle != Provider.of(context).selectedProfile + ? Provider.of(context).theme.messageFromOtherBackgroundColor() + : Provider.of(context).theme.messageFromMeBackgroundColor(), + child: message.getPreviewWidget(context)); } else { return Text(""); } diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 47057f74..e1d91280 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -15,6 +16,12 @@ import 'messagebubbledecorations.dart'; // Like MessageBubble but for displaying chat overlay 100/101 invitations // Offers the user an accept/reject button if they don't have a matching contact already class InvitationBubble extends StatefulWidget { + final int overlay; + final String inviteTarget; + final String inviteNick; + + InvitationBubble(this.overlay, this.inviteTarget, this.inviteNick); + @override InvitationBubbleState createState() => InvitationBubbleState(); } @@ -25,32 +32,22 @@ class InvitationBubbleState extends State { @override Widget build(BuildContext context) { - if (Provider.of(context).malformed) { - return MalformedBubble(); - } - - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; - var isGroup = Provider.of(context).overlay == 101; - isAccepted = Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget) != null; - var prettyDate = ""; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; + var isGroup = widget.overlay == 101; + isAccepted = Provider.of(context).contactList.getContact(widget.inviteTarget) != null; var borderRadiousEh = 15.0; var showGroupInvite = Provider.of(context).isExperimentEnabled(TapirGroupsExperiment); - rejected = Provider.of(context).flags & 0x01 == 0x01; - var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); - - if (Provider.of(context).timestamp != null) { - // user-configurable timestamps prolly ideal? #todo - prettyDate = DateFormat.yMd().add_jm().format(Provider.of(context).timestamp); - } + rejected = Provider.of(context).flags & 0x01 == 0x01; + var prettyDate = DateFormat.yMd().add_jm().format(Provider.of(context).timestamp); // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; - if (!fromMe && Provider.of(context).senderOnion != null) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); + if (!fromMe) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { - senderDisplayStr = Provider.of(context).senderOnion; + senderDisplayStr = Provider.of(context).senderHandle; } } @@ -61,7 +58,7 @@ class InvitationBubbleState extends State { // If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from // some kind of malfeasance. - var selfInvite = Provider.of(context).inviteNick == Provider.of(context).onion; + var selfInvite = widget.inviteNick == Provider.of(context).onion; if (selfInvite) { return MalformedBubble(); } @@ -69,16 +66,15 @@ class InvitationBubbleState extends State { var wdgMessage = isGroup && !showGroupInvite ? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) : fromMe - ? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation, - isGroup ? Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget)!.nickname : Provider.of(context).message, myKey) - : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, Provider.of(context).inviteNick, - Provider.of(context).inviteTarget, myKey)); + ? senderInviteChrome( + AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of(context).contactList.getContact(widget.inviteTarget)!.nickname : widget.inviteTarget) + : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget)); Widget wdgDecorations; if (isGroup && !showGroupInvite) { wdgDecorations = Text('\u202F'); } else if (fromMe) { - wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); } else if (isAccepted) { wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F'); } else if (this.rejected) { @@ -131,22 +127,22 @@ class InvitationBubbleState extends State { setState(() { var profileOnion = Provider.of(context, listen: false).onion; var contact = Provider.of(context, listen: false).onion; - var idx = Provider.of(context, listen: false).messageIndex; - Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); - Provider.of(context).flags |= 0x01; + var idx = Provider.of(context, listen: false).messageIndex; + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); + Provider.of(context).flags |= 0x01; }); } void _btnAccept() { setState(() { var profileOnion = Provider.of(context, listen: false).onion; - Provider.of(context, listen: false).cwtch.ImportBundle(profileOnion, Provider.of(context, listen: false).message); + Provider.of(context, listen: false).cwtch.ImportBundle(profileOnion, widget.inviteTarget); isAccepted = true; }); } // Construct an invite chrome for the sender - Widget senderInviteChrome(String chrome, String targetName, String myKey) { + Widget senderInviteChrome(String chrome, String targetName) { return Wrap(children: [ SelectableText( chrome + '\u202F', @@ -159,7 +155,6 @@ class InvitationBubbleState extends State { ), SelectableText( targetName + '\u202F', - key: Key(myKey), style: TextStyle( color: Provider.of(context).theme.messageFromMeTextColor(), ), @@ -171,7 +166,7 @@ class InvitationBubbleState extends State { } // Construct an invite chrome - Widget inviteChrome(String chrome, String targetName, String targetId, String myKey) { + Widget inviteChrome(String chrome, String targetName, String targetId) { return Wrap(children: [ SelectableText( chrome + '\u202F', @@ -184,7 +179,6 @@ class InvitationBubbleState extends State { ), SelectableText( targetName + '\u202F', - key: Key(myKey), style: TextStyle(color: Provider.of(context).theme.messageFromOtherTextColor()), textAlign: TextAlign.left, maxLines: 2, diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index b6dde480..094e0ff9 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -1,3 +1,4 @@ +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -8,6 +9,10 @@ import '../settings.dart'; import 'messagebubbledecorations.dart'; class MessageBubble extends StatefulWidget { + final String content; + + MessageBubble(this.content); + @override MessageBubbleState createState() => MessageBubbleState(); } @@ -17,33 +22,30 @@ class MessageBubbleState extends State { @override Widget build(BuildContext context) { - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; var prettyDate = ""; var borderRadiousEh = 15.0; - var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); + // var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); - if (Provider.of(context).timestamp != null) { - // user-configurable timestamps prolly ideal? #todo - DateTime messageDate = Provider.of(context).timestamp; - prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); - } + DateTime messageDate = Provider.of(context).timestamp; + prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; - if (!fromMe && Provider.of(context).senderOnion != null) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); + if (!fromMe) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { - senderDisplayStr = Provider.of(context).senderOnion; + senderDisplayStr = Provider.of(context).senderHandle; } } var wdgSender = SelectableText(senderDisplayStr, 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), + widget.content + '\u202F', + //key: Key(myKey), focusNode: _focus, style: TextStyle( color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), @@ -52,9 +54,9 @@ class MessageBubbleState extends State { textWidthBasis: TextWidthBasis.longestLine, ); - var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); - var error = Provider.of(context).error; + var error = Provider.of(context).error; return LayoutBuilder(builder: (context, constraints) { //print(constraints.toString()+", "+constraints.maxWidth.toString()); diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 5e985b7d..0ae041e6 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -1,7 +1,12 @@ +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/messages/malformedmessage.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../main.dart'; import '../model.dart'; import '../settings.dart'; import 'messagerow.dart'; @@ -68,22 +73,22 @@ class _MessageListState extends State { itemCount: Provider.of(outerContext).totalMessages, reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... itemBuilder: (itemBuilderContext, index) { - var trueIndex = Provider.of(outerContext).totalMessages - index - 1; - return ChangeNotifierProvider( - key: ValueKey(trueIndex), - create: (x) => MessageState( - context: itemBuilderContext, - profileOnion: Provider.of(outerContext, listen: false).onion, - // We don't want to listen for updates to the contact handle... - contactHandle: Provider.of(x, listen: false).onion, - messageIndex: trueIndex, - ), - builder: (bcontext, child) { - String idx = Provider.of(outerContext).isGroup == true && Provider.of(bcontext).signature.isEmpty == false - ? Provider.of(bcontext).signature - : trueIndex.toString(); - return RepaintBoundary(child: MessageRow(key: Provider.of(bcontext).getMessageKey(idx))); - }); + var profileOnion = Provider.of(outerContext, listen: false).onion; + var contactHandle = Provider.of(outerContext, listen: false).onion; + var messageIndex = Provider.of(outerContext).totalMessages - index - 1; + + return FutureBuilder( + future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), + builder: (context, snapshot) { + if (snapshot.hasData) { + var message = snapshot.data as Message; + // Already includes MessageRow,, + return message.getWidget(context); + } else { + return MessageLoadingBubble(); + } + }, + ); }, ) : null))) diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index e37e7a85..29b7cfff 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:cwtch/widgets/quotedmessage.dart'; +import 'package:cwtch/models/message.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:provider/provider.dart'; @@ -9,36 +9,29 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; import '../model.dart'; import '../settings.dart'; -import 'invitationbubble.dart'; -import 'malformedbubble.dart'; -import 'messagebubble.dart'; -import 'messageloadingbubble.dart'; class MessageRow extends StatefulWidget { - MessageRow({Key? key}) : super(key: key); + final Widget child; + MessageRow(this.child, {Key? key}) : super(key: key); @override - _MessageRowState createState() => _MessageRowState(); + MessageRowState createState() => MessageRowState(); } -class _MessageRowState extends State { +class MessageRowState extends State { + bool showMenu = false; + @override Widget build(BuildContext context) { - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; - var malformed = Provider.of(context).malformed; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; - // 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: Provider.of(context).loaded == true ? widgetForOverlay(Provider.of(context).overlay) : MessageLoadingBubble()); - Widget wdgIcons = IconButton( - onPressed: () { - Provider.of(context, listen: false).selectedIndex = Provider.of(context).messageIndex; - }, - icon: Icon(Icons.reply, color: Provider.of(context).theme.dropShadowColor())); + Widget wdgIcons = Visibility( + visible: this.showMenu, + child: IconButton( + onPressed: () { + Provider.of(context, listen: false).selectedIndex = Provider.of(context).messageIndex; + }, + icon: Icon(Icons.reply, color: Provider.of(context).theme.dropShadowColor()))); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); var widgetRow = []; @@ -46,7 +39,7 @@ class _MessageRowState extends State { widgetRow = [ wdgSpacer, wdgIcons, - wdgBubble, + Flexible(flex: 3, fit: FlexFit.loose, child: widget.child), ]; } else { var contact = Provider.of(context); @@ -56,7 +49,7 @@ class _MessageRowState extends State { padding: EdgeInsets.all(4.0), child: ProfileImage( diameter: 48.0, - imagePath: Provider.of(context).senderImage ?? 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(), badgeTextColor: Colors.red, badgeColor: Colors.red, @@ -64,30 +57,36 @@ class _MessageRowState extends State { widgetRow = [ wdgPortrait, - wdgBubble, + Flexible(flex: 3, fit: FlexFit.loose, child: widget.child), wdgIcons, wdgSpacer, ]; } - return Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow)); - } + return MouseRegion( + // For desktop... - Widget widgetForOverlay(int o) { - switch (o) { - case 1: - return MessageBubble(); - case 100: - case 101: - return InvitationBubble(); - case 10: - return QuotedMessageBubble(); - } - return MalformedBubble(); + onHover: (event) { + setState(() { + this.showMenu = true; + }); + }, + onExit: (event) { + setState(() { + this.showMenu = false; + }); + }, + child: GestureDetector( + + // Swipe to quote + onHorizontalDragEnd: (details) { + Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageIndex; + }, + child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow)))); } void _btnAdd() { - var sender = Provider.of(context, listen: false).senderOnion; + var sender = Provider.of(context, listen: false).senderHandle; if (sender == null || sender == "") { print("sender not yet loaded"); return; diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index e71d615d..74227242 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -1,7 +1,6 @@ -import 'dart:convert'; - -import 'package:cwtch/main.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../model.dart'; @@ -10,23 +9,12 @@ import 'package:intl/intl.dart'; import '../settings.dart'; import 'messagebubbledecorations.dart'; -class LocallyIndexedMessage { - final dynamic message; - final int index; - - LocallyIndexedMessage(this.message, this.index); - - LocallyIndexedMessage.fromJson(Map json) - : message = json['Message'], - index = json['LocalIndex']; - - Map toJson() => { - 'Message': message, - 'LocalIndex': index, - }; -} - class QuotedMessageBubble extends StatefulWidget { + final Future quotedMessage; + final String body; + + QuotedMessageBubble(this.body, this.quotedMessage); + @override QuotedMessageBubbleState createState() => QuotedMessageBubbleState(); } @@ -36,115 +24,85 @@ class QuotedMessageBubbleState extends State { @override Widget build(BuildContext context) { - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; var prettyDate = ""; var borderRadiousEh = 15.0; - var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); - try { - dynamic message = jsonDecode(Provider.of(context).message); + DateTime messageDate = Provider.of(context).timestamp; + prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); - var quotedMessagePotentials = - Provider.of(context).cwtch.GetMessageByContentHash(Provider.of(context).profileOnion, Provider.of(context).contactHandle, message["quotedHash"]); - int messageIndex = Provider.of(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).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(context).timestamp != null) { - // user-configurable timestamps prolly ideal? #todo - DateTime messageDate = Provider.of(context).timestamp; - prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); + // If the sender is not us, then we want to give them a nickname... + var senderDisplayStr = ""; + if (!fromMe) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + if (contact != null) { + senderDisplayStr = contact.nickname; + } else { + senderDisplayStr = Provider.of(context).senderHandle; } - - // If the sender is not us, then we want to give them a nickname... - var senderDisplayStr = ""; - if (!fromMe && Provider.of(context).senderOnion != null) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); - if (contact != null) { - senderDisplayStr = contact.nickname; - } else { - senderDisplayStr = Provider.of(context).senderOnion; - } - } - var wdgSender = SelectableText(senderDisplayStr, - style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); - - var wdgMessage = SelectableText( - (message["body"] ?? "") + '\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 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(context).theme.messageFromOtherBackgroundColor() : Provider.of(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(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); - - var error = Provider.of(context).error; - - return LayoutBuilder(builder: (context, constraints) { - return RepaintBoundary( - child: Container( - child: Container( - decoration: BoxDecoration( - color: error - ? malformedColor - : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), - border: Border.all( - color: error - ? malformedColor - : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), - width: 1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(borderRadiousEh), - topRight: Radius.circular(borderRadiousEh), - bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, - bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), - ), - ), - child: Padding( - padding: EdgeInsets.all(9.0), - child: Column( - crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgQuote, wdgMessage, wdgDecorations] : [wdgSender, wdgQuote, wdgMessage, wdgDecorations]))))); - }); - } catch (e) { - return MalformedBubble(); } + var wdgSender = SelectableText(senderDisplayStr, + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); + + var wdgMessage = SelectableText( + widget.body + '\u202F', + focusNode: _focus, + style: TextStyle( + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + ); + + var wdgQuote = FutureBuilder( + future: widget.quotedMessage, + builder: (context, snapshot) { + if (snapshot.hasData) { + var qMessage = (snapshot.data! as Message); + // Swap the background color for quoted tweets.. + return Container( + margin: EdgeInsets.all(5), + padding: EdgeInsets.all(5), + color: fromMe ? Provider.of(context).theme.messageFromOtherBackgroundColor() : Provider.of(context).theme.messageFromMeBackgroundColor(), + child: qMessage.getPreviewWidget(context)); + } else { + // This should be almost instantly resolved, any failure likely means an issue in decoding... + return MessageLoadingBubble(); + } + }, + ); + + var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + + var error = Provider.of(context).error; + + return LayoutBuilder(builder: (context, constraints) { + return RepaintBoundary( + child: Container( + child: Container( + decoration: BoxDecoration( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + border: Border.all( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(borderRadiousEh), + topRight: Radius.circular(borderRadiousEh), + bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, + bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + ), + ), + child: Padding( + padding: EdgeInsets.all(9.0), + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgQuote, wdgMessage, wdgDecorations] : [wdgSender, wdgQuote, wdgMessage, wdgDecorations]))))); + }); } } diff --git a/pubspec.lock b/pubspec.lock index 2d7c5500..2ee3b476 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -375,7 +375,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" + version: "0.4.1" typed_data: dependency: transitive description: