Refactor Message/MessageState to make adding more Message Types simpler

This commit is contained in:
Sarah Jamie Lewis 2021-07-06 12:46:39 -07:00
parent b9984a3598
commit ddfc7fc43c
14 changed files with 563 additions and 394 deletions

View File

@ -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<MessageState>(key.currentContext!, listen: false);
var message = Provider.of<MessageMetadata>(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<MessageState>(key.currentContext!, listen: false);
var message = Provider.of<MessageMetadata>(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<MessageState>(key!.currentContext!, listen: false);
var message = Provider.of<MessageMetadata>(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<MessageState>(key.currentContext!, listen: false);
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.error = true;
} catch (e) {

View File

@ -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<String, GlobalKey<MessageBubbleState>> keys;
late Map<String, GlobalKey<MessageRowState>> 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<String, GlobalKey<MessageBubbleState>>();
keys = Map<String, GlobalKey<MessageRowState>>();
}
String get nickname => this._nickname;
@ -451,137 +452,11 @@ class ContactInfoState extends ChangeNotifier {
}
}
GlobalKey<MessageBubbleState> getMessageKey(String index) {
GlobalKey<MessageRowState> getMessageKey(String index) {
if (keys[index] == null) {
keys[index] = GlobalKey<MessageBubbleState>();
keys[index] = GlobalKey<MessageRowState>();
}
GlobalKey<MessageBubbleState> ret = keys[index]!;
GlobalKey<MessageRowState> 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<FlwtchState>(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<ProfileInfoState>(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;
}
});
}
}

100
lib/models/message.dart Normal file
View File

@ -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<Message> messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) {
try {
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(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);
}

View File

@ -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<ContactInfoState>(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<ProfileInfoState>(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<ContactInfoState>(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<ProfileInfoState>(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;
}
}

View File

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

View File

@ -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<String, dynamic> json)
: message = json['Message'],
index = json['LocalIndex'];
Map<String, dynamic> 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<FlwtchState>(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<dynamic>).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<ContactInfoState>(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<ContactInfoState>(bcontext).getMessageKey(idx));
});
} catch (e) {
return MalformedMessage(this.metadata).getWidget(context);
}
}
}

View File

@ -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<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
return MessageRow(MessageBubble(this.content), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
});
}
}

View File

@ -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<MessageView> {
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
Provider.of<FlwtchState>(context)
.cwtch
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(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<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
} catch (e) {
}
Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper();
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(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<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
} catch (e) {}
Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper();
});
} else {
} else {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
@ -201,24 +201,17 @@ class _MessageViewState extends State<MessageView> {
var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
var quoted = FutureBuilder(
future: Provider.of<FlwtchState>(context)
.cwtch
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!),
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(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<AppState>(context).selectedProfile
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()
: Provider.of<Settings>(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<AppState>(context).selectedProfile
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
child: message.getPreviewWidget(context));
} else {
return Text("");
}

View File

@ -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<InvitationBubble> {
@override
Widget build(BuildContext context) {
if (Provider.of<MessageState>(context).malformed) {
return MalformedBubble();
}
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var isGroup = Provider.of<MessageState>(context).overlay == 101;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget) != null;
var prettyDate = "";
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isGroup = widget.overlay == 101;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget) != null;
var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageState>(context).flags & 0x01 == 0x01;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
if (Provider.of<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo
prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp);
}
rejected = Provider.of<MessageMetadata>(context).flags & 0x01 == 0x01;
var prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
@ -61,7 +58,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
// 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<MessageState>(context).inviteNick == Provider.of<ProfileInfoState>(context).onion;
var selfInvite = widget.inviteNick == Provider.of<ProfileInfoState>(context).onion;
if (selfInvite) {
return MalformedBubble();
}
@ -69,16 +66,15 @@ class InvitationBubbleState extends State<InvitationBubble> {
var wdgMessage = isGroup && !showGroupInvite
? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning)
: fromMe
? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation,
isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget)!.nickname : Provider.of<MessageState>(context).message, myKey)
: (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, Provider.of<MessageState>(context).inviteNick,
Provider.of<MessageState>(context).inviteTarget, myKey));
? senderInviteChrome(
AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of<ProfileInfoState>(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<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(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<InvitationBubble> {
setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
var idx = Provider.of<MessageState>(context, listen: false).messageIndex;
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageState>(context, listen: false).flags | 0x01);
Provider.of<MessageState>(context).flags |= 0x01;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01);
Provider.of<MessageMetadata>(context).flags |= 0x01;
});
}
void _btnAccept() {
setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, Provider.of<MessageState>(context, listen: false).message);
Provider.of<FlwtchState>(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<InvitationBubble> {
),
SelectableText(
targetName + '\u202F',
key: Key(myKey),
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
@ -171,7 +166,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
}
// 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<InvitationBubble> {
),
SelectableText(
targetName + '\u202F',
key: Key(myKey),
style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: TextAlign.left,
maxLines: 2,

View File

@ -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<MessageBubble> {
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var prettyDate = "";
var borderRadiousEh = 15.0;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
// var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
if (Provider.of<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo
DateTime messageDate = Provider.of<MessageState>(context).timestamp;
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
}
DateTime messageDate = Provider.of<MessageMetadata>(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<MessageState>(context).senderOnion != null) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
var wdgSender = SelectableText(senderDisplayStr,
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),
widget.content + '\u202F',
//key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
@ -52,9 +54,9 @@ class MessageBubbleState extends State<MessageBubble> {
textWidthBasis: TextWidthBasis.longestLine,
);
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var error = Provider.of<MessageState>(context).error;
var error = Provider.of<MessageMetadata>(context).error;
return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString());

View File

@ -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<MessageList> {
itemCount: Provider.of<ContactInfoState>(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<ContactInfoState>(outerContext).totalMessages - index - 1;
return ChangeNotifierProvider(
key: ValueKey(trueIndex),
create: (x) => MessageState(
context: itemBuilderContext,
profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
// We don't want to listen for updates to the contact handle...
contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion,
messageIndex: trueIndex,
),
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false
? Provider.of<MessageState>(bcontext).signature
: trueIndex.toString();
return RepaintBoundary(child: MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)));
});
var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion;
var messageIndex = Provider.of<ContactInfoState>(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)))

View File

@ -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<MessageRow> {
class MessageRowState extends State<MessageRow> {
bool showMenu = false;
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var malformed = Provider.of<MessageState>(context).malformed;
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(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<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble());
Widget wdgIcons = IconButton(
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageState>(context).messageIndex;
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor()));
Widget wdgIcons = Visibility(
visible: this.showMenu,
child: IconButton(
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context).messageIndex;
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[];
@ -46,7 +39,7 @@ class _MessageRowState extends State<MessageRow> {
widgetRow = <Widget>[
wdgSpacer,
wdgIcons,
wdgBubble,
Flexible(flex: 3, fit: FlexFit.loose, child: widget.child),
];
} else {
var contact = Provider.of<ContactInfoState>(context);
@ -56,7 +49,7 @@ class _MessageRowState extends State<MessageRow> {
padding: EdgeInsets.all(4.0),
child: ProfileImage(
diameter: 48.0,
imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath,
imagePath: Provider.of<MessageMetadata>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
badgeTextColor: Colors.red, badgeColor: Colors.red,
@ -64,30 +57,36 @@ class _MessageRowState extends State<MessageRow> {
widgetRow = <Widget>[
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<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(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<MessageState>(context, listen: false).senderOnion;
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") {
print("sender not yet loaded");
return;

View File

@ -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<String, dynamic> json)
: message = json['Message'],
index = json['LocalIndex'];
Map<String, dynamic> toJson() => {
'Message': message,
'LocalIndex': index,
};
}
class QuotedMessageBubble extends StatefulWidget {
final Future<Message> quotedMessage;
final String body;
QuotedMessageBubble(this.body, this.quotedMessage);
@override
QuotedMessageBubbleState createState() => QuotedMessageBubbleState();
}
@ -36,115 +24,85 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var prettyDate = "";
var borderRadiousEh = 15.0;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
try {
dynamic message = jsonDecode(Provider.of<MessageState>(context).message);
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
var quotedMessagePotentials =
Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(Provider.of<MessageState>(context).profileOnion, Provider.of<MessageState>(context).contactHandle, message["quotedHash"]);
int messageIndex = Provider.of<MessageState>(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<dynamic>).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<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo
DateTime messageDate = Provider.of<MessageState>(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<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
}
}
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText(
(message["body"] ?? "") + '\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 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<Settings>(context).theme.messageFromOtherBackgroundColor() : Provider.of<Settings>(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<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var error = Provider.of<MessageState>(context).error;
return LayoutBuilder(builder: (context, constraints) {
return RepaintBoundary(
child: Container(
child: Container(
decoration: BoxDecoration(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
border: Border.all(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(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<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText(
widget.body + '\u202F',
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 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<Settings>(context).theme.messageFromOtherBackgroundColor() : Provider.of<Settings>(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<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var error = Provider.of<MessageMetadata>(context).error;
return LayoutBuilder(builder: (context, constraints) {
return RepaintBoundary(
child: Container(
child: Container(
decoration: BoxDecoration(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
border: Border.all(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(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])))));
});
}
}

View File

@ -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: