diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index d5ca9f05..080fef9f 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -v1.0.0-70-gafa6794-2021-07-07-01-24 \ No newline at end of file +v1.0.0-25-g801a805-2021-07-07-16-10 \ No newline at end of file diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 89d56826..e70c1c4e 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -126,7 +126,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val profile = (a.get("profile") as? String) ?: "" val handle = (a.get("contact") as? String) ?: "" val contentHash = (a.get("contentHash") as? String) ?: "" - return Result.success(Data.Builder().putString("result", Cwtch.getMessageByContentHash(profile, handle, contentHash)).build()) + return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, handle, contentHash)).build()) } "UpdateMessageFlags" -> { val profile = (a.get("profile") as? String) ?: "" diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 5f0c70a1..4981e504 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,8 @@ { "@@locale": "de", - "@@last_modified": "2021-07-05T21:26:10+02:00", + "@@last_modified": "2021-07-07T18:42:50+02:00", + "tooltipRemoveThisQuotedMessage": "Remove quoted message.", + "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "Neue Nachricht in einer Gruppe!", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 208e19f7..20ef3d49 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,8 @@ { "@@locale": "en", - "@@last_modified": "2021-07-05T21:26:10+02:00", + "@@last_modified": "2021-07-07T18:42:50+02:00", + "tooltipRemoveThisQuotedMessage": "Remove quoted message.", + "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "New message in a group!", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index f5b3f7cd..0e22d582 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,8 @@ { "@@locale": "es", - "@@last_modified": "2021-07-05T21:26:10+02:00", + "@@last_modified": "2021-07-07T18:42:50+02:00", + "tooltipRemoveThisQuotedMessage": "Remove quoted message.", + "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "New message in a group!", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 39b89c1b..3ef29e60 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,8 @@ { "@@locale": "fr", - "@@last_modified": "2021-07-05T21:26:10+02:00", + "@@last_modified": "2021-07-07T18:42:50+02:00", + "tooltipRemoveThisQuotedMessage": "Remove quoted message.", + "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Refuser cette demande de contact", "tooltipAcceptContactRequest": "Acceptez cette demande de contact.", "notificationNewMessageFromGroup": "Nouveau message dans un groupe !", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 8d95c3da..f076374d 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,8 @@ { "@@locale": "it", - "@@last_modified": "2021-07-05T21:26:10+02:00", + "@@last_modified": "2021-07-07T18:42:50+02:00", + "tooltipRemoveThisQuotedMessage": "Remove quoted message.", + "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Rifiuta questa richiesta di contatto", "tooltipAcceptContactRequest": "Accetta questa richiesta di contatto.", "notificationNewMessageFromGroup": "Nuovo messaggio in un gruppo!", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 44c464c4..bd4e9125 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,8 @@ { "@@locale": "pl", - "@@last_modified": "2021-07-05T21:26:10+02:00", + "@@last_modified": "2021-07-07T18:42:50+02:00", + "tooltipRemoveThisQuotedMessage": "Remove quoted message.", + "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "New message in a group!", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index ab682cfa..e71ae5a6 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,8 @@ { "@@locale": "pt", - "@@last_modified": "2021-06-29T19:15:43+02:00", + "@@last_modified": "2021-07-07T18:42:50+02:00", + "tooltipRemoveThisQuotedMessage": "Remove quoted message.", + "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "New message in a group!", @@ -42,16 +44,10 @@ "tooltipAddContact": "Add a new contact or conversation", "titleManageContacts": "Conversations", "titleManageServers": "Manage Servers", - "dateMonthsAgo": "Months Ago", "dateNever": "Never", - "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateLastYear": "Last Year", "dateYesterday": "Yesterday", "dateLastMonth": "Last Month", - "dateWeeksAgo": "Weeks Ago", - "dateDaysAgo": "Days Ago", - "dateHoursAgo": "Hours Ago", - "dateMinutesAgo": "Minutes Ago", "dateRightNow": "Right Now", "successfullAddedContact": "Successfully added ", "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", diff --git a/lib/models/message.dart b/lib/models/message.dart index 4a84f6b5..de53c977 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -8,6 +8,17 @@ import 'messages/malformedmessage.dart'; import 'messages/quotedmessage.dart'; import 'messages/textmessage.dart'; +// Define the overlays +const TextMessageOverlay = 1; +const QuotedMessageOverlay = 10; +const SuggestContactOverlay = 100; +const InviteGroupOverlay = 101; + +// Defines the length of the tor v3 onion address. Code using this constant will +// need to updated when we allow multiple different identifiers. At which time +// it will likely be prudent to define a proper Contact wrapper. +const TorV3ContactHandleLength = 56; + abstract class Message { MessageMetadata getMetadata(); Widget getWidget(BuildContext context); @@ -19,16 +30,22 @@ Future messageHandler(BuildContext context, String profileOnion, String var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); + // There are 2 conditions in which this error condition can be met: + // 1. The application == nil, in which case this instance of the UI is already + // broken beyond repair, and will either be replaced by a new version, or requires a complete + // restart. + // 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. + // This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the + // calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one + // will find itself delayed. + // The second case is recoverable by tail-recursing this future. if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { + print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); 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']; @@ -36,26 +53,32 @@ Future messageHandler(BuildContext context, String profileOnion, String 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); + try { + dynamic message = jsonDecode(messageWrapper['Message']); + var content = message['d'] as dynamic; + var overlay = int.parse(message['o'].toString()); + + switch (overlay) { + case TextMessageOverlay: + return TextMessage(metadata, content); + case SuggestContactOverlay: + case InviteGroupOverlay: + return InviteMessage(overlay, metadata, content); + case QuotedMessageOverlay: + return QuotedMessage(metadata, content); + default: + // Metadata is valid, content is not.. + return MalformedMessage(metadata); + } + } catch (e) { + return MalformedMessage(metadata); } }); } catch (e) { diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index 5fa36dde..6e31abb8 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -3,7 +3,6 @@ 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'; @@ -27,7 +26,7 @@ class InviteMessage extends Message { String inviteTarget; String inviteNick; - if (this.content.length == 56) { + if (this.content.length == TorV3ContactHandleLength) { inviteTarget = this.content; var targetContact = Provider.of(context).contactList.getContact(inviteTarget); inviteNick = targetContact == null ? this.content : targetContact.nickname; @@ -53,7 +52,7 @@ class InviteMessage extends Message { String inviteTarget; String inviteNick; - if (this.content.length == 56) { + if (this.content.length == TorV3ContactHandleLength) { inviteTarget = this.content; var targetContact = Provider.of(context).contactList.getContact(inviteTarget); inviteNick = targetContact == null ? this.content : targetContact.nickname; diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index b1b44254..d1210ff4 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -2,7 +2,6 @@ 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'; @@ -39,7 +38,7 @@ class QuotedMessage extends Message { builder: (bcontext, child) { try { dynamic message = jsonDecode(this.content); - return MessageBubble(message["body"]); + return Text(message["body"]); } catch (e) { return MalformedMessage(this.metadata).getWidget(context); } @@ -55,10 +54,12 @@ class QuotedMessage extends Message { Widget getWidget(BuildContext context) { try { dynamic message = jsonDecode(this.content); - var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]); int messageIndex = metadata.messageIndex; - var quotedMessage = quotedMessagePotentials.then((matchingMessages) { + Future quotedMessage = quotedMessagePotentials.then((matchingMessages) { + if (matchingMessages == "[]") { + return null; + } // 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 @@ -79,15 +80,16 @@ class QuotedMessage extends Message { builder: (bcontext, child) { String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); return MessageRow( - QuotedMessageBubble(message["body"], quotedMessage.then((localIndex) { + QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { if (localIndex != null) { return messageHandler(context, metadata.profileOnion, metadata.contactHandle, localIndex.index); } - return Future.value(MalformedMessage(this.metadata)); + return MalformedMessage(this.metadata); })), key: Provider.of(bcontext).getMessageKey(idx)); }); } catch (e) { + print("Quoted message exception" + e.toString()); return MalformedMessage(this.metadata).getWidget(context); } } diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index 716c38ae..37375f8f 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -9,6 +9,7 @@ import '../../model.dart'; class TextMessage extends Message { final MessageMetadata metadata; final String content; + TextMessage(this.metadata, this.content); @override @@ -16,7 +17,7 @@ class TextMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - return MessageBubble(this.content); + return Text(this.content); }); } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 3c9abcf6..af0a2c80 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -4,6 +4,7 @@ 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/messageloadingbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -117,7 +118,7 @@ class _MessageViewState extends State { 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); + ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage); Provider.of(context, listen: false) .cwtch .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); @@ -126,7 +127,7 @@ class _MessageViewState extends State { _sendMessageHelper(); }); } else { - ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); + ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text); Provider.of(context, listen: false) .cwtch .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); @@ -211,9 +212,21 @@ class _MessageViewState extends State { color: message.getMetadata().senderHandle != Provider.of(context).selectedProfile ? Provider.of(context).theme.messageFromOtherBackgroundColor() : Provider.of(context).theme.messageFromMeBackgroundColor(), - child: message.getPreviewWidget(context)); + child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ + Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.reply, size: 32))), + Center(widthFactor: 1.0, child: message.getPreviewWidget(context)), + Center( + widthFactor: 1.0, + child: IconButton( + icon: Icon(Icons.highlight_remove), + tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage, + onPressed: () { + Provider.of(context, listen: false).selectedIndex = null; + }, + )) + ])); } else { - return Text(""); + return MessageLoadingBubble(); } }, ); diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index e1d91280..0ff47c02 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -33,7 +33,7 @@ class InvitationBubbleState extends State { @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; - var isGroup = widget.overlay == 101; + var isGroup = widget.overlay == InviteGroupOverlay; isAccepted = Provider.of(context).contactList.getContact(widget.inviteTarget) != null; var borderRadiousEh = 15.0; var showGroupInvite = Provider.of(context).isExperimentEnabled(TapirGroupsExperiment); diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 29b7cfff..cb007acc 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -28,6 +28,7 @@ class MessageRowState extends State { Widget wdgIcons = Visibility( visible: this.showMenu, child: IconButton( + tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage, onPressed: () { Provider.of(context, listen: false).selectedIndex = Provider.of(context).messageIndex; }, diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index 74227242..466325b6 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -58,13 +58,21 @@ class QuotedMessageBubbleState extends State { future: widget.quotedMessage, builder: (context, snapshot) { if (snapshot.hasData) { - var qMessage = (snapshot.data! as Message); - // Swap the background color for quoted tweets.. - return Container( - margin: EdgeInsets.all(5), - padding: EdgeInsets.all(5), - color: fromMe ? Provider.of(context).theme.messageFromOtherBackgroundColor() : Provider.of(context).theme.messageFromMeBackgroundColor(), - child: qMessage.getPreviewWidget(context)); + try { + var qMessage = (snapshot.data! as Message); + // Swap the background color for quoted tweets.. + return Container( + margin: EdgeInsets.all(5), + padding: EdgeInsets.all(5), + color: fromMe ? Provider.of(context).theme.messageFromOtherBackgroundColor() : Provider.of(context).theme.messageFromMeBackgroundColor(), + child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ + Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.reply, size: 32))), + Center(widthFactor: 1.0, child: qMessage.getPreviewWidget(context)) + ])); + } catch (e) { + print(e); + return MalformedBubble(); + } } else { // This should be almost instantly resolved, any failure likely means an issue in decoding... return MessageLoadingBubble();