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 'dart:convert';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
import 'package:cwtch/notification_manager.dart'; import 'package:cwtch/notification_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -115,7 +116,7 @@ class CwtchNotifier {
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break; if (message == null) break;
message.ackd = true; message.ackd = true;
} catch (e) { } catch (e) {
@ -138,7 +139,7 @@ class CwtchNotifier {
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx); var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break; if (message == null) break;
message.ackd = true; message.ackd = true;
} catch (e) { } catch (e) {
@ -156,7 +157,7 @@ class CwtchNotifier {
var idx = data["Index"]; var idx = data["Index"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx);
try { try {
var message = Provider.of<MessageState>(key!.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key!.currentContext!, listen: false);
message.error = true; message.error = true;
} catch (e) { } catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature // 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); var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break; if (message == null) break;
message.error = true; message.error = true;
} catch (e) { } catch (e) {

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/messagebubble.dart'; import 'package:cwtch/widgets/messagebubble.dart';
@ -343,7 +344,7 @@ class ContactInfoState extends ChangeNotifier {
late int _unreadMessages = 0; late int _unreadMessages = 0;
late int _totalMessages = 0; late int _totalMessages = 0;
late DateTime _lastMessageTime; 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" // todo: a nicer way to model contacts, groups and other "entities"
late bool _isGroup; late bool _isGroup;
@ -375,7 +376,7 @@ class ContactInfoState extends ChangeNotifier {
this._savePeerHistory = savePeerHistory; this._savePeerHistory = savePeerHistory;
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server; this._server = server;
keys = Map<String, GlobalKey<MessageBubbleState>>(); keys = Map<String, GlobalKey<MessageRowState>>();
} }
String get nickname => this._nickname; 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) { if (keys[index] == null) {
keys[index] = GlobalKey<MessageBubbleState>(); keys[index] = GlobalKey<MessageRowState>();
} }
GlobalKey<MessageBubbleState> ret = keys[index]!; GlobalKey<MessageRowState> ret = keys[index]!;
return ret; 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 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/cupertino.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) { if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
Provider.of<FlwtchState>(context) Provider.of<FlwtchState>(context)
.cwtch .cwtch
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!).then((data) { .GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!)
try { .then((data) {
var messageWrapper = jsonDecode(data! as String); try {
var bytes1 = utf8.encode(messageWrapper["PeerID"]+messageWrapper['Message']); var messageWrapper = jsonDecode(data! as String);
var digest1 = sha256.convert(bytes1); var bytes1 = utf8.encode(messageWrapper["PeerID"] + messageWrapper['Message']);
var contentHash = base64Encode(digest1.bytes); var digest1 = sha256.convert(bytes1);
var quotedMessage = "{\"quotedHash\":\""+contentHash+"\",\"body\":\""+ctrlrCompose.value.text+"\"}"; var contentHash = base64Encode(digest1.bytes);
ChatMessage cm = new ChatMessage(o: 10, d: quotedMessage); var quotedMessage = "{\"quotedHash\":\"" + contentHash + "\",\"body\":\"" + ctrlrCompose.value.text + "\"}";
Provider.of<FlwtchState>(context, listen: false) ChatMessage cm = new ChatMessage(o: 10, d: quotedMessage);
.cwtch Provider.of<FlwtchState>(context, listen: false)
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm)); .cwtch
} catch (e) { .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;
Provider.of<AppState>(context, listen: false).selectedIndex = null; _sendMessageHelper();
_sendMessageHelper();
}); });
} else { } else {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
@ -201,24 +201,17 @@ class _MessageViewState extends State<MessageView> {
var children; var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) { if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
var quoted = FutureBuilder( var quoted = FutureBuilder(
future: Provider.of<FlwtchState>(context) future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!),
.cwtch
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
try { var message = snapshot.data! as Message;
var messageWrapper = jsonDecode(snapshot.data! as String); return Container(
dynamic message = jsonDecode(messageWrapper['Message']); margin: EdgeInsets.all(5),
return Container( padding: EdgeInsets.all(5),
margin: EdgeInsets.all(5), color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
padding: EdgeInsets.all(5), ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()
color: messageWrapper['PeerID'] != Provider.of<AppState>(context).selectedProfile : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor() child: message.getPreviewWidget(context));
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
child: Text(message["d"]));
} catch (e) {
return MalformedBubble();
}
} else { } else {
return Text(""); return Text("");
} }

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -15,6 +16,12 @@ import 'messagebubbledecorations.dart';
// Like MessageBubble but for displaying chat overlay 100/101 invitations // 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 // Offers the user an accept/reject button if they don't have a matching contact already
class InvitationBubble extends StatefulWidget { class InvitationBubble extends StatefulWidget {
final int overlay;
final String inviteTarget;
final String inviteNick;
InvitationBubble(this.overlay, this.inviteTarget, this.inviteNick);
@override @override
InvitationBubbleState createState() => InvitationBubbleState(); InvitationBubbleState createState() => InvitationBubbleState();
} }
@ -25,32 +32,22 @@ class InvitationBubbleState extends State<InvitationBubble> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (Provider.of<MessageState>(context).malformed) { var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
return MalformedBubble(); var isGroup = widget.overlay == 101;
} isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget) != null;
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 borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment); var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageState>(context).flags & 0x01 == 0x01; rejected = Provider.of<MessageMetadata>(context).flags & 0x01 == 0x01;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString(); var prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
if (Provider.of<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo
prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp);
}
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } 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 // 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. // 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) { if (selfInvite) {
return MalformedBubble(); return MalformedBubble();
} }
@ -69,16 +66,15 @@ class InvitationBubbleState extends State<InvitationBubble> {
var wdgMessage = isGroup && !showGroupInvite var wdgMessage = isGroup && !showGroupInvite
? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) ? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning)
: fromMe : fromMe
? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation, ? senderInviteChrome(
isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget)!.nickname : Provider.of<MessageState>(context).message, myKey) 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, Provider.of<MessageState>(context).inviteNick, : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget));
Provider.of<MessageState>(context).inviteTarget, myKey));
Widget wdgDecorations; Widget wdgDecorations;
if (isGroup && !showGroupInvite) { if (isGroup && !showGroupInvite) {
wdgDecorations = Text('\u202F'); wdgDecorations = Text('\u202F');
} else if (fromMe) { } 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) { } else if (isAccepted) {
wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F'); wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F');
} else if (this.rejected) { } else if (this.rejected) {
@ -131,22 +127,22 @@ class InvitationBubbleState extends State<InvitationBubble> {
setState(() { setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion; var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
var idx = Provider.of<MessageState>(context, listen: false).messageIndex; var idx = Provider.of<MessageMetadata>(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<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01);
Provider.of<MessageState>(context).flags |= 0x01; Provider.of<MessageMetadata>(context).flags |= 0x01;
}); });
} }
void _btnAccept() { void _btnAccept() {
setState(() { setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; 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; isAccepted = true;
}); });
} }
// Construct an invite chrome for the sender // Construct an invite chrome for the sender
Widget senderInviteChrome(String chrome, String targetName, String myKey) { Widget senderInviteChrome(String chrome, String targetName) {
return Wrap(children: [ return Wrap(children: [
SelectableText( SelectableText(
chrome + '\u202F', chrome + '\u202F',
@ -159,7 +155,6 @@ class InvitationBubbleState extends State<InvitationBubble> {
), ),
SelectableText( SelectableText(
targetName + '\u202F', targetName + '\u202F',
key: Key(myKey),
style: TextStyle( style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
), ),
@ -171,7 +166,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
} }
// Construct an invite chrome // 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: [ return Wrap(children: [
SelectableText( SelectableText(
chrome + '\u202F', chrome + '\u202F',
@ -184,7 +179,6 @@ class InvitationBubbleState extends State<InvitationBubble> {
), ),
SelectableText( SelectableText(
targetName + '\u202F', targetName + '\u202F',
key: Key(myKey),
style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()), style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: TextAlign.left, textAlign: TextAlign.left,
maxLines: 2, maxLines: 2,

View File

@ -1,3 +1,4 @@
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -8,6 +9,10 @@ import '../settings.dart';
import 'messagebubbledecorations.dart'; import 'messagebubbledecorations.dart';
class MessageBubble extends StatefulWidget { class MessageBubble extends StatefulWidget {
final String content;
MessageBubble(this.content);
@override @override
MessageBubbleState createState() => MessageBubbleState(); MessageBubbleState createState() => MessageBubbleState();
} }
@ -17,33 +22,30 @@ class MessageBubbleState extends State<MessageBubble> {
@override @override
Widget build(BuildContext context) { 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 prettyDate = "";
var borderRadiousEh = 15.0; 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) { DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
// user-configurable timestamps prolly ideal? #todo prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
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... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion; senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
} }
} }
var wdgSender = SelectableText(senderDisplayStr, var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())); style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText( var wdgMessage = SelectableText(
(Provider.of<MessageState>(context).message ?? "") + '\u202F', widget.content + '\u202F',
key: Key(myKey), //key: Key(myKey),
focusNode: _focus, focusNode: _focus,
style: TextStyle( style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(), 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, 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) { return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString()); //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/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagerow.dart'; import 'messagerow.dart';
@ -68,22 +73,22 @@ class _MessageListState extends State<MessageList> {
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages, 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... 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) { itemBuilder: (itemBuilderContext, index) {
var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1; var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
return ChangeNotifierProvider( var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion;
key: ValueKey(trueIndex), var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
create: (x) => MessageState(
context: itemBuilderContext, return FutureBuilder(
profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion, future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
// We don't want to listen for updates to the contact handle... builder: (context, snapshot) {
contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion, if (snapshot.hasData) {
messageIndex: trueIndex, var message = snapshot.data as Message;
), // Already includes MessageRow,,
builder: (bcontext, child) { return message.getWidget(context);
String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false } else {
? Provider.of<MessageState>(bcontext).signature return MessageLoadingBubble();
: trueIndex.toString(); }
return RepaintBoundary(child: MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx))); },
}); );
}, },
) )
: null))) : null)))

View File

@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/widgets/quotedmessage.dart'; import 'package:cwtch/models/message.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -9,36 +9,29 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../settings.dart'; import '../settings.dart';
import 'invitationbubble.dart';
import 'malformedbubble.dart';
import 'messagebubble.dart';
import 'messageloadingbubble.dart';
class MessageRow extends StatefulWidget { class MessageRow extends StatefulWidget {
MessageRow({Key? key}) : super(key: key); final Widget child;
MessageRow(this.child, {Key? key}) : super(key: key);
@override @override
_MessageRowState createState() => _MessageRowState(); MessageRowState createState() => MessageRowState();
} }
class _MessageRowState extends State<MessageRow> { class MessageRowState extends State<MessageRow> {
bool showMenu = false;
@override @override
Widget build(BuildContext context) { 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 malformed = Provider.of<MessageState>(context).malformed;
// If the message is malformed then override fromme as we can't trust it Widget wdgIcons = Visibility(
if (malformed) { visible: this.showMenu,
fromMe = false; child: IconButton(
} onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context).messageIndex;
Widget wdgBubble = },
Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble()); icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
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 wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[]; var widgetRow = <Widget>[];
@ -46,7 +39,7 @@ class _MessageRowState extends State<MessageRow> {
widgetRow = <Widget>[ widgetRow = <Widget>[
wdgSpacer, wdgSpacer,
wdgIcons, wdgIcons,
wdgBubble, Flexible(flex: 3, fit: FlexFit.loose, child: widget.child),
]; ];
} else { } else {
var contact = Provider.of<ContactInfoState>(context); var contact = Provider.of<ContactInfoState>(context);
@ -56,7 +49,7 @@ class _MessageRowState extends State<MessageRow> {
padding: EdgeInsets.all(4.0), padding: EdgeInsets.all(4.0),
child: ProfileImage( child: ProfileImage(
diameter: 48.0, diameter: 48.0,
imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath, imagePath: Provider.of<MessageMetadata>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated", //maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(), border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
badgeTextColor: Colors.red, badgeColor: Colors.red, badgeTextColor: Colors.red, badgeColor: Colors.red,
@ -64,30 +57,36 @@ class _MessageRowState extends State<MessageRow> {
widgetRow = <Widget>[ widgetRow = <Widget>[
wdgPortrait, wdgPortrait,
wdgBubble, Flexible(flex: 3, fit: FlexFit.loose, child: widget.child),
wdgIcons, wdgIcons,
wdgSpacer, 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) { onHover: (event) {
switch (o) { setState(() {
case 1: this.showMenu = true;
return MessageBubble(); });
case 100: },
case 101: onExit: (event) {
return InvitationBubble(); setState(() {
case 10: this.showMenu = false;
return QuotedMessageBubble(); });
} },
return MalformedBubble(); 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() { void _btnAdd() {
var sender = Provider.of<MessageState>(context, listen: false).senderOnion; var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") { if (sender == null || sender == "") {
print("sender not yet loaded"); print("sender not yet loaded");
return; return;

View File

@ -1,7 +1,6 @@
import 'dart:convert'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/main.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../model.dart'; import '../model.dart';
@ -10,23 +9,12 @@ import 'package:intl/intl.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.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 { class QuotedMessageBubble extends StatefulWidget {
final Future<Message> quotedMessage;
final String body;
QuotedMessageBubble(this.body, this.quotedMessage);
@override @override
QuotedMessageBubbleState createState() => QuotedMessageBubbleState(); QuotedMessageBubbleState createState() => QuotedMessageBubbleState();
} }
@ -36,115 +24,85 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
@override @override
Widget build(BuildContext context) { 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 prettyDate = "";
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
try { DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
dynamic message = jsonDecode(Provider.of<MessageState>(context).message); prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
var quotedMessagePotentials = // If the sender is not us, then we want to give them a nickname...
Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(Provider.of<MessageState>(context).profileOnion, Provider.of<MessageState>(context).contactHandle, message["quotedHash"]); var senderDisplayStr = "";
int messageIndex = Provider.of<MessageState>(context).messageIndex; if (!fromMe) {
var quotedMessage = quotedMessagePotentials.then((matchingMessages) { ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
// reverse order the messages from newest to oldest and return the if (contact != null) {
// first matching message where it's index is less than the index of this senderDisplayStr = contact.nickname;
// message } else {
try { senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
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 && 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 name: charcode
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -375,7 +375,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.0" version: "0.4.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description: