From 370a7fd1dad9321471b86479ba16f3a8b7e38fe1 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 5 Jul 2021 12:30:26 -0700 Subject: [PATCH 1/7] Add Polish Template --- lib/l10n/intl_de.arb | 8 +-- lib/l10n/intl_en.arb | 8 +-- lib/l10n/intl_es.arb | 8 +-- lib/l10n/intl_fr.arb | 40 +++++------ lib/l10n/intl_it.arb | 126 ++++++++++++++++----------------- lib/widgets/quotedmessage.dart | 89 +++++++++++++++++++++++ 6 files changed, 169 insertions(+), 110 deletions(-) create mode 100644 lib/widgets/quotedmessage.dart diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 17575393..5f0c70a1 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,6 @@ { "@@locale": "de", - "@@last_modified": "2021-06-29T19:15:43+02:00", + "@@last_modified": "2021-07-05T21:26:10+02:00", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "Neue Nachricht in einer Gruppe!", @@ -42,16 +42,10 @@ "tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen", "titleManageContacts": "Unterhaltungen", "titleManageServers": "Server verwalten", - "dateMonthsAgo": "Months Ago", "dateNever": "Nie", - "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateLastYear": "Letzes Jahr", "dateYesterday": "Gestern", "dateLastMonth": "Letzter Monat", - "dateWeeksAgo": "Weeks Ago", - "dateDaysAgo": "Days Ago", - "dateHoursAgo": "Hours Ago", - "dateMinutesAgo": "Minutes Ago", "dateRightNow": "Jetzt", "successfullAddedContact": "Erfolgreich hinzugefügt", "descriptionBlockUnknownConnections": "Falls aktiviert, wird diese Einstellung alle Verbindungen von Cwtch Usern autmoatisch schliessen, wenn sie nicht in deinen Kontakten sind.", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 7d6a2fac..208e19f7 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2021-06-29T19:15:43+02:00", + "@@last_modified": "2021-07-05T21:26:10+02:00", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "New message in a group!", @@ -42,16 +42,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/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 9046ba26..f5b3f7cd 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,6 @@ { "@@locale": "es", - "@@last_modified": "2021-06-29T19:15:43+02:00", + "@@last_modified": "2021-07-05T21:26:10+02:00", "tooltipRejectContactRequest": "Reject this contact request", "tooltipAcceptContactRequest": "Accept this contact request.", "notificationNewMessageFromGroup": "New message in a group!", @@ -42,16 +42,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/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index a08e85e1..39b89c1b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,18 +1,18 @@ { "@@locale": "fr", - "@@last_modified": "2021-06-29T19:15:43+02:00", - "tooltipRejectContactRequest": "Reject this contact request", - "tooltipAcceptContactRequest": "Accept this contact request.", + "@@last_modified": "2021-07-05T21:26:10+02:00", + "tooltipRejectContactRequest": "Refuser cette demande de contact", + "tooltipAcceptContactRequest": "Acceptez cette demande de contact.", "notificationNewMessageFromGroup": "Nouveau message dans un groupe !", "notificationNewMessageFromPeer": "Nouveau message d'un contact !", "tooltipHidePassword": "Masquer le mot de passe", "tooltipShowPassword": "Afficher le mot de passe", "serverNotSynced": "Synchronisation des nouveaux messages (Cela peut prendre un certain temps)...", "groupInviteSettingsWarning": "Vous avez été invité à rejoindre un groupe ! Veuillez activer l'expérience de discussion de groupe dans les paramètres pour afficher cette invitation.", - "shutdownCwtchAction": "Arrêt Cwtch", + "shutdownCwtchAction": "Arrêt de Cwtch", "shutdownCwtchDialog": "Êtes-vous sûr de vouloir arrêter Cwtch ? Ceci fermera toutes les connexions, et quittera l'application.", "shutdownCwtchDialogTitle": "Arrêter Cwtch ?", - "shutdownCwtchTooltip": "Arrêt Cwtch", + "shutdownCwtchTooltip": "Arrêt de Cwtch", "malformedMessage": "Message mal formé", "profileDeleteSuccess": "Le profil a été supprimé avec succès", "debugLog": "Activer le journal de la console de débogage", @@ -42,16 +42,10 @@ "tooltipAddContact": "Ajouter un nouveau contact ou une nouvelle conversation", "titleManageContacts": "Conversations", "titleManageServers": "Gérer les serveurs", - "dateMonthsAgo": "Il y a plusieurs mois", "dateNever": "Jamais", - "dateYearsAgo": "Il y a X ans (affiché à côté d'une ligne de contact pour indiquer l'heure de la dernière action)", "dateLastYear": "L'année dernière", "dateYesterday": "Hier", "dateLastMonth": "Le mois dernier", - "dateWeeksAgo": "Il y a quelques semaines", - "dateDaysAgo": "Il y a quelques jours", - "dateHoursAgo": "Il y a quelques heures", - "dateMinutesAgo": "Il y a quelques minutes", "dateRightNow": "Maintenant", "successfullAddedContact": "Ajouté avec succès ", "descriptionBlockUnknownConnections": "Si elle est activée, cette option fermera automatiquement les connexions des utilisateurs de Cwtch qui n'ont pas été ajoutés à votre liste de contacts.", @@ -66,7 +60,7 @@ "enterCurrentPasswordForDelete": "Veuillez entrer le mot de passe actuel pour supprimer ce profil.", "enableGroups": "Activer la discussion de groupe", "experimentsEnabled": "Activer les expériences", - "localeIt": "Italienne", + "localeIt": "Italien", "localeEs": "Espagnol", "addListItem": "Ajouter un nouvel élément de liste", "addNewItem": "Ajouter un nouvel élément à la liste", @@ -80,9 +74,9 @@ "loadingTor": "Chargement de tor...", "smallTextLabel": "Petit", "defaultScalingText": "Taille par défaut du texte (échelle:", - "builddate": "Construit sur : 2%", - "version": "Version 1%", - "versionTor": "Version 1% avec tor 2%", + "builddate": "Construit le : %2", + "version": "Version %1", + "versionTor": "Version %1 avec tor %2", "themeDark": "Sombre", "themeLight": "Clair", "settingTheme": "Thème", @@ -94,8 +88,8 @@ "localeEn": "Anglais", "settingLanguage": "Langue", "blockUnknownLabel": "Bloquer les pairs inconnus", - "zoomLabel": "Interface zoom (essentiellement la taille du texte et des composants de l'interface)", - "versionBuilddate": "Version 1% avec tor 2%", + "zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)", + "versionBuilddate": "Version : %1 Construite le : %2", "cwtchSettingsTitle": "Préférences Cwtch", "unlock": "Déverrouiller", "yourServers": "Vos serveurs", @@ -105,7 +99,7 @@ "enterProfilePassword": "Entrez un mot de passe pour consulter vos profils", "addNewProfileBtn": "Ajouter un nouveau profil", "deleteConfirmText": "SUPPRIMER", - "deleteProfileConfirmBtn": "Vraiment supprimer le profil", + "deleteProfileConfirmBtn": "Supprimer vraiment le profil ?", "deleteConfirmLabel": "Tapez SUPPRIMER pour confirmer", "deleteProfileBtn": "Supprimer le profil", "passwordChangeError": "Erreur lors de la modification du mot de passe : le mot de passe fourni est rejeté", @@ -129,7 +123,7 @@ "profileName": "Pseudo", "editProfileTitle": "Modifier le profil", "addProfileTitle": "Ajouter un nouveau profil", - "deleteBtn": "Effacer", + "deleteBtn": "Supprimer", "unblockBtn": "Débloquer le pair", "dontSavePeerHistory": "Supprimer l'historique des pairs", "savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au pair.", @@ -150,16 +144,16 @@ "peerOfflineMessage": "Le pair est hors ligne, les messages ne peuvent pas être remis pour le moment", "peerBlockedMessage": "Le pair est bloqué", "pendingLabel": "En attente", - "acknowledgedLabel": "Confirmé", + "acknowledgedLabel": "Accusé de réception", "couldNotSendMsgError": "Impossible d'envoyer ce message", "dmTooltip": "Envoyer un message privé", - "membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe.", + "membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être représentatives de l'ensemble des membres du groupe.", "addListItemBtn": "Ajouter un élément", "peerNotOnline": "Le pair est hors ligne, les messages ne peuvent pas être remis pour le moment", "searchList": "Liste de recherche", "update": "Mise à jour", "inviteBtn": "Invitation", - "inviteToGroupLabel": "Inviter quelqu'un", + "inviteToGroupLabel": "Inviter au groupe", "groupNameLabel": "Nom du groupe", "viewServerInfo": "Informations sur le serveur", "serverSynced": "Synchronisé", @@ -192,6 +186,6 @@ "createGroupTab": "Créer un groupe", "addPeerTab": "Ajouter un pair", "createGroupBtn": "Créer", - "defaultGroupName": "Un super groupe", + "defaultGroupName": "Un groupe génial", "createGroupTitle": "Créer un groupe" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index d4300a5b..8d95c3da 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,71 +1,65 @@ { "@@locale": "it", - "@@last_modified": "2021-06-29T19:15:43+02:00", - "tooltipRejectContactRequest": "Reject this contact request", - "tooltipAcceptContactRequest": "Accept this contact request.", - "notificationNewMessageFromGroup": "New message in a group!", - "notificationNewMessageFromPeer": "New message from a contact!", - "tooltipHidePassword": "Hide Password", - "tooltipShowPassword": "Show Password", - "serverNotSynced": "Non sincronizzato", - "groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", - "shutdownCwtchAction": "Shutdown Cwtch", - "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", - "shutdownCwtchDialogTitle": "Shutdown Cwtch?", - "shutdownCwtchTooltip": "Shutdown Cwtch", - "malformedMessage": "Malformed message", - "profileDeleteSuccess": "Successfully deleted profile", - "debugLog": "Turn on console debug logging", - "torNetworkStatus": "Tor network status", - "addContactFirst": "Add or pick a contact to begin chatting.", - "createProfileToBegin": "Please create or unlock a profile to begin", - "nickChangeSuccess": "Profile nickname changed successfully", - "addServerFirst": "You need to add a server before you can create a group", - "deleteProfileSuccess": "Successfully deleted profile", - "sendInvite": "Send a contact or group invite", - "sendMessage": "Send Message", - "cancel": "Cancel", - "resetTor": "Reset", - "torStatus": "Tor Status", - "torVersion": "Tor Version", - "sendAnInvitation": "You sent an invitation for: ", - "contactSuggestion": "This is a contact suggestion for: ", - "rejected": "Rejected!", - "accepted": "Accepted!", - "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", - "newPassword": "New Password", - "yesLeave": "Yes, Leave This Conversation", - "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", - "leaveGroup": "Leave This Conversation", - "inviteToGroup": "You have been invited to join a group:", - "pasteAddressToAddContact": "... incolla qui un indirizzo per aggiungere un contatto...", - "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.", - "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", - "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", - "titleManageProfiles": "Manage Cwtch Profiles", - "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", - "tooltipOpenSettings": "Open the settings pane", - "invalidImportString": "Invalid import string", - "contactAlreadyExists": "Contact Already Exists", - "conversationSettings": "Conversation Settings", - "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", - "enableGroups": "Enable Group Chat", - "experimentsEnabled": "Esperimenti abilitati", + "@@last_modified": "2021-07-05T21:26:10+02:00", + "tooltipRejectContactRequest": "Rifiuta questa richiesta di contatto", + "tooltipAcceptContactRequest": "Accetta questa richiesta di contatto.", + "notificationNewMessageFromGroup": "Nuovo messaggio in un gruppo!", + "notificationNewMessageFromPeer": "Nuovo messaggio da un contatto!", + "tooltipHidePassword": "Nascondi la password", + "tooltipShowPassword": "Mostra la password", + "serverNotSynced": "Sincronizzazione nuovi messaggi (l'operazione può richiedere del tempo)...", + "groupInviteSettingsWarning": "Sei stato invitato ad unirti ad un gruppo! Abilita l'Esperimento di chat di gruppo in Impostazioni per visualizzare questo Invito.", + "shutdownCwtchAction": "Chiudi Cwtch", + "shutdownCwtchDialog": "Sei sicuro di voler chiudere Cwtch? Questo chiuderà tutte le connessioni e uscirà dall'applicazione.", + "shutdownCwtchDialogTitle": "Chiudi Cwtch?", + "shutdownCwtchTooltip": "Chiudi Cwtch", + "malformedMessage": "Messaggio non valido", + "profileDeleteSuccess": "Profilo eliminato con successo", + "debugLog": "Attiva la registrazione del debug della console", + "torNetworkStatus": "Stato della rete Tor", + "addContactFirst": "Aggiungi o scegli un contatto per iniziare a chattare.", + "createProfileToBegin": "Crea o sblocca un profilo per iniziare", + "nickChangeSuccess": "Nickname del profilo modificato con successo", + "addServerFirst": "È necessario aggiungere un server prima di poter creare un gruppo", + "deleteProfileSuccess": "Profilo eliminato con successo", + "sendInvite": "Invia un invito a un contatto o a un gruppo", + "sendMessage": "Invia messaggio", + "cancel": "Annulla", + "resetTor": "Resettare", + "torStatus": "Stato di Tor", + "torVersion": "Versione di Tor", + "sendAnInvitation": "Hai inviato un invito per:", + "contactSuggestion": "Questo è un suggerimento di contatto per:", + "rejected": "Rifiutato!", + "accepted": "Accettato!", + "chatHistoryDefault": "Questa conversazione sarà cancellata quando Cwtch sarà chiuso! La cronologia dei messaggi può essere abilitata per ogni conversazione tramite il menu Impostazioni in alto a destra.", + "newPassword": "Nuova password", + "yesLeave": "Sì, lascia questa conversazione", + "reallyLeaveThisGroupPrompt": "Uscire da questa conversazione? Tutti i messaggi e gli attributi verranno eliminati.", + "leaveGroup": "Lascia questa conversazione", + "inviteToGroup": "Hai ricevuto un invito a unirti a un gruppo:", + "pasteAddressToAddContact": "Incolla qui un indirizzo cwtch, un invito o un mazzo di chiavi per aggiungere una nuova conversazione", + "tooltipAddContact": "Aggiungi un nuovo contatto o conversazione", + "titleManageContacts": "Conversazioni", + "titleManageServers": "Gestisci i server", + "dateNever": "Mai", + "dateLastYear": "L'anno scorso", + "dateYesterday": "Ieri", + "dateLastMonth": "Mese scorso", + "dateRightNow": "Ora", + "successfullAddedContact": "Aggiunto con successo ", + "descriptionBlockUnknownConnections": "Se attivata, questa opzione chiuderà automaticamente le connessioni degli utenti Cwtch che non sono stati aggiunti alla tua lista di contatti.", + "descriptionExperimentsGroups": "L'esperimento di gruppo permette a Cwtch di connettersi con un'infrastruttura server non fidata per facilitare la comunicazione con più di un contatto.", + "descriptionExperiments": "Gli esperimenti di Cwtch sono opzioni a scelta che aggiungono a Cwtch funzionalità che possono avere diverse considerazioni sulla privacy rispetto alla tradizionale chat 1:1 resistente ai metadati, ad esempio chat di gruppo, integrazione di bot ecc.", + "titleManageProfiles": "Gestisci i profili Cwtch", + "tooltipUnlockProfiles": "Sblocca i profili crittografati inserendo la loro password.", + "tooltipOpenSettings": "Aprire il pannello delle impostazioni", + "invalidImportString": "Importazione stringa non valida", + "contactAlreadyExists": "Il contatto esiste già", + "conversationSettings": "Impostazioni di conversazione", + "enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.", + "enableGroups": "Abilita la chat di gruppo", + "experimentsEnabled": "Abilita esperimenti", "localeIt": "Italiano", "localeEs": "Spagnolo", "addListItem": "Aggiungi un nuovo elemento alla lista", diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart new file mode 100644 index 00000000..a0cba993 --- /dev/null +++ b/lib/widgets/quotedmessage.dart @@ -0,0 +1,89 @@ +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../model.dart'; +import 'package:intl/intl.dart'; + +import '../settings.dart'; +import 'messagebubbledecorations.dart'; + +class QuotedMessageBubble extends StatefulWidget { + @override + QuotedMessageBubbleState createState() => QuotedMessageBubbleState(); +} + +class QuotedMessageBubbleState extends State { + FocusNode _focus = FocusNode(); + + @override + Widget build(BuildContext context) { + var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; + var prettyDate = ""; + var borderRadiousEh = 15.0; + var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); + + if (Provider.of(context).timestamp != null) { + // user-configurable timestamps prolly ideal? #todo + DateTime messageDate = Provider.of(context).timestamp; + prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); + } + + // If the sender is not us, then we want to give them a nickname... + var senderDisplayStr = ""; + if (!fromMe && Provider.of(context).senderOnion != null) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); + if (contact != null) { + senderDisplayStr = contact.nickname; + } else { + senderDisplayStr = Provider.of(context).senderOnion; + } + } + var wdgSender = SelectableText(senderDisplayStr, + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); + + var wdgMessage = SelectableText( + (Provider.of(context).message ?? "") + '\u202F', + key: Key(myKey), + focusNode: _focus, + style: TextStyle( + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + ); + + var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + + var error = Provider.of(context).error; + + return LayoutBuilder(builder: (context, constraints) { + //print(constraints.toString()+", "+constraints.maxWidth.toString()); + return RepaintBoundary( + child: Container( + child: Container( + decoration: BoxDecoration( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + border: Border.all( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(borderRadiousEh), + topRight: Radius.circular(borderRadiousEh), + bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, + bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + ), + ), + child: Padding( + padding: EdgeInsets.all(9.0), + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); + }); + } +} From b9984a35986cd5495f7e8bf53c406ea445ffd8c3 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 5 Jul 2021 12:31:16 -0700 Subject: [PATCH 2/7] Quoted Messages Initial Pass --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 6 + lib/cwtch/cwtch.dart | 2 + lib/cwtch/ffi.dart | 19 +- lib/cwtch/gomobile.dart | 5 + lib/model.dart | 11 +- lib/views/messageview.dart | 144 ++++++++++----- lib/widgets/contactrow.dart | 1 + lib/widgets/messagerow.dart | 13 +- lib/widgets/quotedmessage.dart | 173 ++++++++++++------ pubspec.lock | 7 + pubspec.yaml | 1 + 11 files changed, 273 insertions(+), 109 deletions(-) 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 fddf4372..89d56826 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -122,6 +122,12 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val indexI = a.getInt("index") return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) } + "GetMessageByContentHash" -> { + 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()) + } "UpdateMessageFlags" -> { val profile = (a.get("profile") as? String) ?: "" val handle = (a.get("contact") as? String) ?: "" diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 6d9a3036..25359975 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -32,6 +32,8 @@ abstract class Cwtch { // ignore: non_constant_identifier_names Future GetMessage(String profile, String handle, int index); // ignore: non_constant_identifier_names + Future GetMessageByContentHash(String profile, String handle, String contentHash); + // ignore: non_constant_identifier_names void UpdateMessageFlags(String profile, String handle, int index, int flags); // ignore: non_constant_identifier_names void SendMessage(String profile, String handle, String message); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 0f8f1ae3..a1b4bda7 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -57,9 +57,9 @@ typedef GetIntFromStrStrFn = int Function(Pointer, int, Pointer, int typedef get_json_blob_from_str_str_int_function = Pointer Function(Pointer, Int32, Pointer, Int32, Int32); typedef GetJsonBlobFromStrStrIntFn = Pointer Function(Pointer, int, Pointer, int, int); -//func GetMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, start C.int, end C.int) *C.char { -typedef get_json_blob_from_str_str_int_int_function = Pointer Function(Pointer, Int32, Pointer, Int32, Int32, Int32); -typedef GetJsonBlobFromStrStrIntIntFn = Pointer Function(Pointer, int, Pointer, int, int, int); +// func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char +typedef get_json_blob_from_str_str_str_function = Pointer Function(Pointer, Int32, Pointer, Int32, Pointer, Int32); +typedef GetJsonBlobFromStrStrStrFn = Pointer Function(Pointer, int, Pointer, int, Pointer, int); typedef appbus_events_function = Pointer Function(); typedef AppbusEventsFn = Pointer Function(); @@ -397,4 +397,17 @@ class CwtchFfi implements Cwtch { _receivePort.close(); print("Receive Port Closed"); } + + @override + Future GetMessageByContentHash(String profile, String handle, String contentHash) async { + var getMessagesByContentHashC = library.lookup>("c_GetMessagesByContentHash"); + // ignore: non_constant_identifier_names + final GetMessagesByContentHash = getMessagesByContentHashC.asFunction(); + final utf8profile = profile.toNativeUtf8(); + final utf8handle = handle.toNativeUtf8(); + final utf8contentHash = contentHash.toNativeUtf8(); + Pointer jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length); + String jsonMessage = jsonMessageBytes.toDartString(); + return jsonMessage; + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index d9be08c9..4717f7e5 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -183,4 +183,9 @@ class CwtchGomobile implements Cwtch { print("gomobile.dart Shutdown"); cwtchPlatform.invokeMethod("Shutdown", {}); } + + @override + Future GetMessageByContentHash(String profile, String handle, String contentHash) { + return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "contentHash": contentHash}); + } } diff --git a/lib/model.dart b/lib/model.dart index 32dbb485..667264c7 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -67,6 +67,7 @@ class AppState extends ChangeNotifier { String appError = ""; String? _selectedProfile; String? _selectedConversation; + int? _selectedIndex; void SetCwtchInit() { cwtchInit = true; @@ -90,6 +91,12 @@ class AppState extends ChangeNotifier { notifyListeners(); } + int? get selectedIndex => _selectedIndex; + set selectedIndex(int? newVal) { + this._selectedIndex = newVal; + notifyListeners(); + } + bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; } @@ -457,7 +464,7 @@ class MessageState extends ChangeNotifier { final String profileOnion; final String contactHandle; final int messageIndex; - late String _message; + late dynamic _message; late int _overlay; late String _inviteTarget; late String _inviteNick; @@ -535,7 +542,7 @@ class MessageState extends ChangeNotifier { return; } dynamic message = jsonDecode(messageWrapper['Message']); - this._message = message['d']; + this._message = message['d'] as dynamic; this._overlay = int.parse(message['o'].toString()); this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; this._senderOnion = messageWrapper['PeerID']; diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 258f596e..c8d276b6 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -1,7 +1,8 @@ import 'dart:convert'; import 'dart:io'; - +import 'package:crypto/crypto.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -104,11 +105,33 @@ class _MessageViewState extends State { void _sendMessage([String? ignoredParam]) { if (ctrlrCompose.value.text.isNotEmpty) { - ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); - Provider.of(context, listen: false) - .cwtch - .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); - _sendMessageHelper(); + if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { + Provider.of(context) + .cwtch + .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!).then((data) { + try { + var messageWrapper = jsonDecode(data! as String); + var bytes1 = utf8.encode(messageWrapper["PeerID"]+messageWrapper['Message']); + var digest1 = sha256.convert(bytes1); + var contentHash = base64Encode(digest1.bytes); + var quotedMessage = "{\"quotedHash\":\""+contentHash+"\",\"body\":\""+ctrlrCompose.value.text+"\"}"; + ChatMessage cm = new ChatMessage(o: 10, d: quotedMessage); + Provider.of(context, listen: false) + .cwtch + .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); + } catch (e) { + + } + Provider.of(context, listen: false).selectedIndex = null; + _sendMessageHelper(); + }); + } else { + ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); + Provider.of(context, listen: false) + .cwtch + .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); + _sendMessageHelper(); + } } } @@ -130,7 +153,7 @@ class _MessageViewState extends State { } Widget _buildComposeBox() { - return Container( + var composeBox = Container( color: Provider.of(context).theme.backgroundMainColor(), padding: EdgeInsets.all(2), margin: EdgeInsets.all(2), @@ -141,52 +164,83 @@ class _MessageViewState extends State { child: Container( decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of(context).theme.defaultButtonActiveColor()))), child: RawKeyboardListener( - focusNode: FocusNode(), - onKey: handleKeyPress, - child: TextFormField( - key: Key('txtCompose'), - controller: ctrlrCompose, - focusNode: focusNode, - autofocus: !Platform.isAndroid, - textInputAction: TextInputAction.newline, - keyboardType: TextInputType.multiline, - minLines: 1, - maxLines: null, - onFieldSubmitted: _sendMessage, - decoration: InputDecoration( - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - enabled: true, - prefixIcon: IconButton( - icon: Icon(CwtchIcons.send_invite, size: 24, color: Provider.of(context).theme.mainTextColor()), - tooltip: AppLocalizations.of(context)!.sendInvite, - enableFeedback: true, - splashColor: Provider.of(context).theme.defaultButtonActiveColor(), - hoverColor: Provider.of(context).theme.defaultButtonActiveColor(), - onPressed: () => _modalSendInvitation(context)), - suffixIcon: IconButton( - icon: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of(context).theme.mainTextColor()), - tooltip: AppLocalizations.of(context)!.sendMessage, - onPressed: _sendMessage, - ), - )))), + focusNode: FocusNode(), + onKey: handleKeyPress, + child: TextFormField( + key: Key('txtCompose'), + controller: ctrlrCompose, + focusNode: focusNode, + autofocus: !Platform.isAndroid, + textInputAction: TextInputAction.newline, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: null, + onFieldSubmitted: _sendMessage, + decoration: InputDecoration( + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + enabled: true, + prefixIcon: IconButton( + icon: Icon(CwtchIcons.send_invite, size: 24, color: Provider.of(context).theme.mainTextColor()), + tooltip: AppLocalizations.of(context)!.sendInvite, + enableFeedback: true, + splashColor: Provider.of(context).theme.defaultButtonActiveColor(), + hoverColor: Provider.of(context).theme.defaultButtonActiveColor(), + onPressed: () => _modalSendInvitation(context)), + suffixIcon: IconButton( + icon: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of(context).theme.mainTextColor()), + tooltip: AppLocalizations.of(context)!.sendMessage, + onPressed: _sendMessage, + ), + )))), ), ], ), ); + + var children; + if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { + var quoted = FutureBuilder( + future: Provider.of(context) + .cwtch + .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!), + builder: (context, snapshot) { + if (snapshot.hasData) { + try { + var messageWrapper = jsonDecode(snapshot.data! as String); + dynamic message = jsonDecode(messageWrapper['Message']); + return Container( + margin: EdgeInsets.all(5), + padding: EdgeInsets.all(5), + color: messageWrapper['PeerID'] != Provider.of(context).selectedProfile + ? Provider.of(context).theme.messageFromOtherBackgroundColor() + : Provider.of(context).theme.messageFromMeBackgroundColor(), + child: Text(message["d"])); + } catch (e) { + return MalformedBubble(); + } + } else { + return Text(""); + } + }, + ); + + children = [quoted, composeBox]; + } else { + children = [composeBox]; + } + + return Column(mainAxisSize: MainAxisSize.min, children: children); } // Send the message if enter is pressed without the shift key... void handleKeyPress(event) { - var data = event.data as RawKeyEventData; - if (data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) { - final messageWithoutNewLine = ctrlrCompose.value.text.trimRight(); - ctrlrCompose.value = TextEditingValue( - text: messageWithoutNewLine - ); - _sendMessage(); - - } + var data = event.data as RawKeyEventData; + if (data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) { + final messageWithoutNewLine = ctrlrCompose.value.text.trimRight(); + ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine); + _sendMessage(); + } } void placeHolder() => {}; diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index d38d1df1..0c844b00 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -98,6 +98,7 @@ class _ContactRowState extends State { Provider.of(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0; // triggers update in Double/TripleColumnView Provider.of(context, listen: false).selectedConversation = contact.onion; + Provider.of(context, listen: false).selectedIndex = null; // if in singlepane mode, push to the stack var isLandscape = Provider.of(context, listen: false).isLandscape(context); if (Provider.of(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion); diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index b6c99788..e37e7a85 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cwtch/widgets/quotedmessage.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:provider/provider.dart'; @@ -33,14 +34,18 @@ class _MessageRowState extends State { Widget wdgBubble = Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of(context).loaded == true ? widgetForOverlay(Provider.of(context).overlay) : MessageLoadingBubble()); - Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of(context).theme.dropShadowColor()); + Widget wdgIcons = IconButton( + onPressed: () { + Provider.of(context, listen: false).selectedIndex = Provider.of(context).messageIndex; + }, + icon: Icon(Icons.reply, color: Provider.of(context).theme.dropShadowColor())); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); var widgetRow = []; if (fromMe) { widgetRow = [ wdgSpacer, - //wdgIcons, + wdgIcons, wdgBubble, ]; } else { @@ -60,7 +65,7 @@ class _MessageRowState extends State { widgetRow = [ wdgPortrait, wdgBubble, - //wdgIcons, + wdgIcons, wdgSpacer, ]; } @@ -75,6 +80,8 @@ class _MessageRowState extends State { case 100: case 101: return InvitationBubble(); + case 10: + return QuotedMessageBubble(); } return MalformedBubble(); } diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index a0cba993..e71d615d 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:cwtch/main.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -7,6 +10,22 @@ import 'package:intl/intl.dart'; import '../settings.dart'; import 'messagebubbledecorations.dart'; +class LocallyIndexedMessage { + final dynamic message; + final int index; + + LocallyIndexedMessage(this.message, this.index); + + LocallyIndexedMessage.fromJson(Map json) + : message = json['Message'], + index = json['LocalIndex']; + + Map toJson() => { + 'Message': message, + 'LocalIndex': index, + }; +} + class QuotedMessageBubble extends StatefulWidget { @override QuotedMessageBubbleState createState() => QuotedMessageBubbleState(); @@ -22,68 +41,110 @@ class QuotedMessageBubbleState extends State { var borderRadiousEh = 15.0; var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); - if (Provider.of(context).timestamp != null) { - // user-configurable timestamps prolly ideal? #todo - DateTime messageDate = Provider.of(context).timestamp; - prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); - } + try { + dynamic message = jsonDecode(Provider.of(context).message); - // If the sender is not us, then we want to give them a nickname... - var senderDisplayStr = ""; - if (!fromMe && Provider.of(context).senderOnion != null) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); - if (contact != null) { - senderDisplayStr = contact.nickname; - } else { - senderDisplayStr = Provider.of(context).senderOnion; + var quotedMessagePotentials = + Provider.of(context).cwtch.GetMessageByContentHash(Provider.of(context).profileOnion, Provider.of(context).contactHandle, message["quotedHash"]); + int messageIndex = Provider.of(context).messageIndex; + var quotedMessage = quotedMessagePotentials.then((matchingMessages) { + // reverse order the messages from newest to oldest and return the + // first matching message where it's index is less than the index of this + // message + try { + var list = (jsonDecode(matchingMessages) as List).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); + LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () { + return list.firstWhere((element) => messageIndex > element.index); + }); + return candidate; + } catch (e) { + // Malformed Message will be returned... + } + }); + + if (Provider.of(context).timestamp != null) { + // user-configurable timestamps prolly ideal? #todo + DateTime messageDate = Provider.of(context).timestamp; + prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); } - } - var wdgSender = SelectableText(senderDisplayStr, - style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); - var wdgMessage = SelectableText( - (Provider.of(context).message ?? "") + '\u202F', - key: Key(myKey), - focusNode: _focus, - style: TextStyle( - color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), - ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ); + // If the sender is not us, then we want to give them a nickname... + var senderDisplayStr = ""; + if (!fromMe && Provider.of(context).senderOnion != null) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); + if (contact != null) { + senderDisplayStr = contact.nickname; + } else { + senderDisplayStr = Provider.of(context).senderOnion; + } + } + var wdgSender = SelectableText(senderDisplayStr, + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); - var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + var wdgMessage = SelectableText( + (message["body"] ?? "") + '\u202F', + key: Key(myKey), + focusNode: _focus, + style: TextStyle( + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + ); - var error = Provider.of(context).error; + var wdgQuote = FutureBuilder( + future: quotedMessage, + builder: (context, snapshot) { + if (snapshot.hasData) { + var lim = (snapshot.data! as LocallyIndexedMessage); + var limmessage = lim.message; + // Swap the background color for quoted tweets.. + return Container( + margin: EdgeInsets.all(5), + padding: EdgeInsets.all(5), + color: fromMe ? Provider.of(context).theme.messageFromOtherBackgroundColor() : Provider.of(context).theme.messageFromMeBackgroundColor(), + child: Text(jsonDecode(limmessage)["d"])); + } else { + // This should be almost instantly resolved, any failure likely means an issue in decoding... + return MalformedBubble(); + } + }, + ); - return LayoutBuilder(builder: (context, constraints) { - //print(constraints.toString()+", "+constraints.maxWidth.toString()); - return RepaintBoundary( - child: Container( - child: Container( - decoration: BoxDecoration( - color: error - ? malformedColor - : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), - border: Border.all( - color: error - ? malformedColor - : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), - width: 1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(borderRadiousEh), - topRight: Radius.circular(borderRadiousEh), - bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, - bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + + var error = Provider.of(context).error; + + return LayoutBuilder(builder: (context, constraints) { + return RepaintBoundary( + child: Container( + child: Container( + decoration: BoxDecoration( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + border: Border.all( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(borderRadiousEh), + topRight: Radius.circular(borderRadiousEh), + bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, + bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + ), ), - ), - child: Padding( - padding: EdgeInsets.all(9.0), - child: Column( - crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); - }); + 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(); + } } } diff --git a/pubspec.lock b/pubspec.lock index 58f991e0..2d7c5500 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + crypto: + dependency: "direct main" + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" cupertino_icons: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7e23469e..e59cc712 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: ffi: ^1.0.0 path_provider: ^2.0.0 desktop_notifications: 0.5.0 + crypto: 3.0.1 glob: any flutter_test: From ddfc7fc43c76928cd38684d2969b2da4d4f3d13d Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 6 Jul 2021 12:46:39 -0700 Subject: [PATCH 3/7] Refactor Message/MessageState to make adding more Message Types simpler --- lib/cwtch/cwtchNotifier.dart | 9 +- lib/model.dart | 137 +-------------- lib/models/message.dart | 100 +++++++++++ lib/models/messages/invitemessage.dart | 78 +++++++++ lib/models/messages/malformedmessage.dart | 33 ++++ lib/models/messages/quotedmessage.dart | 94 ++++++++++ lib/models/messages/textmessage.dart | 37 ++++ lib/views/messageview.dart | 59 +++---- lib/widgets/invitationbubble.dart | 58 +++--- lib/widgets/messagebubble.dart | 30 ++-- lib/widgets/messagelist.dart | 37 ++-- lib/widgets/messagerow.dart | 77 ++++---- lib/widgets/quotedmessage.dart | 204 +++++++++------------- pubspec.lock | 4 +- 14 files changed, 563 insertions(+), 394 deletions(-) create mode 100644 lib/models/message.dart create mode 100644 lib/models/messages/invitemessage.dart create mode 100644 lib/models/messages/malformedmessage.dart create mode 100644 lib/models/messages/quotedmessage.dart create mode 100644 lib/models/messages/textmessage.dart diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 7551526a..4ecc0ec5 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/notification_manager.dart'; import 'package:provider/provider.dart'; @@ -115,7 +116,7 @@ class CwtchNotifier { var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); if (key == null) break; try { - var message = Provider.of(key.currentContext!, listen: false); + var message = Provider.of(key.currentContext!, listen: false); if (message == null) break; message.ackd = true; } catch (e) { @@ -138,7 +139,7 @@ class CwtchNotifier { var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx); if (key == null) break; try { - var message = Provider.of(key.currentContext!, listen: false); + var message = Provider.of(key.currentContext!, listen: false); if (message == null) break; message.ackd = true; } catch (e) { @@ -156,7 +157,7 @@ class CwtchNotifier { var idx = data["Index"]; var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); try { - var message = Provider.of(key!.currentContext!, listen: false); + var message = Provider.of(key!.currentContext!, listen: false); message.error = true; } catch (e) { // ignore, we likely have an old key that has been replaced with an actual signature @@ -169,7 +170,7 @@ class CwtchNotifier { var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx); if (key == null) break; try { - var message = Provider.of(key.currentContext!, listen: false); + var message = Provider.of(key.currentContext!, listen: false); if (message == null) break; message.error = true; } catch (e) { diff --git a/lib/model.dart b/lib/model.dart index 667264c7..8830c92b 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/cupertino.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/widgets/messagebubble.dart'; @@ -343,7 +344,7 @@ class ContactInfoState extends ChangeNotifier { late int _unreadMessages = 0; late int _totalMessages = 0; late DateTime _lastMessageTime; - late Map> keys; + late Map> keys; // todo: a nicer way to model contacts, groups and other "entities" late bool _isGroup; @@ -375,7 +376,7 @@ class ContactInfoState extends ChangeNotifier { this._savePeerHistory = savePeerHistory; this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._server = server; - keys = Map>(); + keys = Map>(); } String get nickname => this._nickname; @@ -451,137 +452,11 @@ class ContactInfoState extends ChangeNotifier { } } - GlobalKey getMessageKey(String index) { + GlobalKey getMessageKey(String index) { if (keys[index] == null) { - keys[index] = GlobalKey(); + keys[index] = GlobalKey(); } - GlobalKey ret = keys[index]!; + GlobalKey ret = keys[index]!; return ret; } } - -class MessageState extends ChangeNotifier { - final String profileOnion; - final String contactHandle; - final int messageIndex; - late dynamic _message; - late int _overlay; - late String _inviteTarget; - late String _inviteNick; - late DateTime _timestamp; - late String _senderOnion; - late int _flags; - String? _senderImage; - late String _signature = ""; - late bool _ackd = false; - late bool _error = false; - late bool _loaded = false; - late bool _malformed = false; - - MessageState({ - required BuildContext context, - required this.profileOnion, - required this.contactHandle, - required this.messageIndex, - }) { - this._senderOnion = profileOnion; - tryLoad(context); - } - - get message => this._message; - get overlay => this._overlay; - get timestamp => this._timestamp; - int get flags => this._flags; - set flags(int newVal) { - this._flags = newVal; - notifyListeners(); - } - - bool get ackd => this._ackd; - bool get error => this._error; - bool get malformed => this._malformed; - bool get loaded => this._loaded; - get senderOnion => this._senderOnion; - get senderImage => this._senderImage; - get signature => this._signature; - get isInvite => this.overlay == 100 || this.overlay == 101; - get inviteTarget => this._inviteTarget; - get inviteNick => this._inviteNick; - - set ackd(bool newVal) { - this._ackd = newVal; - notifyListeners(); - } - - set error(bool newVal) { - this._error = newVal; - notifyListeners(); - } - - set malformed(bool newVal) { - this._malformed = newVal; - notifyListeners(); - } - - set loaded(bool newVal) { - // quickly-arriving messages get discarded before loading sometimes - if (!hasListeners) return; - this._loaded = newVal; - notifyListeners(); - } - - void tryLoad(BuildContext context) { - Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { - try { - dynamic messageWrapper = jsonDecode(jsonMessage); - if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { - this._senderOnion = profileOnion; - Future.delayed(const Duration(milliseconds: 2), () { - tryLoad(context); - }); - return; - } - dynamic message = jsonDecode(messageWrapper['Message']); - this._message = message['d'] as dynamic; - this._overlay = int.parse(message['o'].toString()); - this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; - this._senderOnion = messageWrapper['PeerID']; - this._senderImage = messageWrapper['ContactImage']; - this._flags = int.parse(messageWrapper['Flags'].toString(), radix: 2); - - // If this is a group, store the signature - if (contactHandle.length == 32) { - this._signature = messageWrapper['Signature']; - } - - // if this is an invite, get the contact handle - if (this.isInvite) { - if (message['d'].toString().length == 56) { - this._inviteTarget = message['d']; - var targetContact = Provider.of(context).contactList.getContact(this._inviteTarget); - this._inviteNick = targetContact == null ? message['d'] : targetContact.nickname; - } else { - var parts = message['d'].toString().split("||"); - if (parts.length == 2) { - var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); - this._inviteTarget = jsonObj['GroupID']; - this._inviteNick = jsonObj['GroupName']; - } - } - } - - this.loaded = true; - - //update ackd and error last as they are changenotified - this.ackd = messageWrapper['Acknowledged']; - if (messageWrapper['Error'] != null) { - this.error = true; - } - } catch (e) { - this._overlay = -1; - this.loaded = true; - this.malformed = true; - } - }); - } -} diff --git a/lib/models/message.dart b/lib/models/message.dart new file mode 100644 index 00000000..4a84f6b5 --- /dev/null +++ b/lib/models/message.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../main.dart'; +import 'messages/invitemessage.dart'; +import 'messages/malformedmessage.dart'; +import 'messages/quotedmessage.dart'; +import 'messages/textmessage.dart'; + +abstract class Message { + MessageMetadata getMetadata(); + Widget getWidget(BuildContext context); + Widget getPreviewWidget(BuildContext context); +} + +Future messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) { + try { + var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); + return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { + dynamic messageWrapper = jsonDecode(rawMessageEnvelope); + if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { + return Future.delayed(Duration(seconds: 2), () { + return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); + }); + } + + dynamic message = jsonDecode(messageWrapper['Message']); + var content = message['d'] as dynamic; + var overlay = int.parse(message['o'].toString()); + + // Construct the initial metadata + var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; + var senderHandle = messageWrapper['PeerID']; + var senderImage = messageWrapper['ContactImage']; + var flags = int.parse(messageWrapper['Flags'].toString(), radix: 2); + var ackd = messageWrapper['Acknowledged']; + var error = messageWrapper['Error'] != null; + + String? signature; + // If this is a group, store the signature + if (contactHandle.length == 32) { + signature = messageWrapper['Signature']; + } + + var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error); + + switch (overlay) { + case 1: + return TextMessage(metadata, content); + case 100: + case 101: + return InviteMessage(overlay, metadata, content); + case 10: + return QuotedMessage(metadata, content); + default: + // Metadata is valid, content is not.. + return MalformedMessage(metadata); + } + }); + } catch (e) { + return Future.value(MalformedMessage(MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true))); + } +} + +class MessageMetadata extends ChangeNotifier { + // meta-metadata + final String profileOnion; + final String contactHandle; + final int messageIndex; + + final DateTime timestamp; + final String senderHandle; + final String? senderImage; + int _flags; + bool _ackd; + bool _error; + + final String? signature; + + int get flags => this._flags; + set flags(int newVal) { + this._flags = newVal; + notifyListeners(); + } + + bool get ackd => this._ackd; + set ackd(bool newVal) { + this._ackd = newVal; + notifyListeners(); + } + + bool get error => this._error; + set error(bool newVal) { + this._error = newVal; + notifyListeners(); + } + + MessageMetadata(this.profileOnion, this.contactHandle, this.messageIndex, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); +} diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart new file mode 100644 index 00000000..5fa36dde --- /dev/null +++ b/lib/models/messages/invitemessage.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; + +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/invitationbubble.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messagebubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../model.dart'; + +class InviteMessage extends Message { + final MessageMetadata metadata; + final String content; + final int overlay; + + InviteMessage(this.overlay, this.metadata, this.content); + + @override + Widget getWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); + + String inviteTarget; + String inviteNick; + + if (this.content.length == 56) { + inviteTarget = this.content; + var targetContact = Provider.of(context).contactList.getContact(inviteTarget); + inviteNick = targetContact == null ? this.content : targetContact.nickname; + } else { + var parts = this.content.toString().split("||"); + if (parts.length == 2) { + var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); + inviteTarget = jsonObj['GroupID']; + inviteNick = jsonObj['GroupName']; + } else { + return MessageRow(MalformedBubble()); + } + } + return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick), key: Provider.of(bcontext).getMessageKey(idx)); + }); + } + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String inviteTarget; + String inviteNick; + + if (this.content.length == 56) { + inviteTarget = this.content; + var targetContact = Provider.of(context).contactList.getContact(inviteTarget); + inviteNick = targetContact == null ? this.content : targetContact.nickname; + } else { + var parts = this.content.toString().split("||"); + if (parts.length == 2) { + var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); + inviteTarget = jsonObj['GroupID']; + inviteNick = jsonObj['GroupName']; + } else { + return MalformedBubble(); + } + } + return InvitationBubble(overlay, inviteTarget, inviteNick); + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } +} diff --git a/lib/models/messages/malformedmessage.dart b/lib/models/messages/malformedmessage.dart new file mode 100644 index 00000000..dc943fe1 --- /dev/null +++ b/lib/models/messages/malformedmessage.dart @@ -0,0 +1,33 @@ +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class MalformedMessage extends Message { + final MessageMetadata metadata; + MalformedMessage(this.metadata); + + @override + Widget getWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (context, child) { + return MessageRow(MalformedBubble()); + }); + } + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + return MalformedBubble(); + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } +} diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart new file mode 100644 index 00000000..b1b44254 --- /dev/null +++ b/lib/models/messages/quotedmessage.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/messages/malformedmessage.dart'; +import 'package:cwtch/widgets/messagebubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:cwtch/widgets/quotedmessage.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../main.dart'; +import '../../model.dart'; + +class LocallyIndexedMessage { + final dynamic message; + final int index; + + LocallyIndexedMessage(this.message, this.index); + + LocallyIndexedMessage.fromJson(Map json) + : message = json['Message'], + index = json['LocalIndex']; + + Map toJson() => { + 'Message': message, + 'LocalIndex': index, + }; +} + +class QuotedMessage extends Message { + final MessageMetadata metadata; + final String content; + QuotedMessage(this.metadata, this.content); + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + try { + dynamic message = jsonDecode(this.content); + return MessageBubble(message["body"]); + } catch (e) { + return MalformedMessage(this.metadata).getWidget(context); + } + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } + + @override + Widget getWidget(BuildContext context) { + try { + dynamic message = jsonDecode(this.content); + + var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]); + int messageIndex = metadata.messageIndex; + var quotedMessage = quotedMessagePotentials.then((matchingMessages) { + // reverse order the messages from newest to oldest and return the + // first matching message where it's index is less than the index of this + // message + try { + var list = (jsonDecode(matchingMessages) as List).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); + LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () { + return list.firstWhere((element) => messageIndex > element.index); + }); + return candidate; + } catch (e) { + // Malformed Message will be returned... + return null; + } + }); + + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); + return MessageRow( + QuotedMessageBubble(message["body"], quotedMessage.then((localIndex) { + if (localIndex != null) { + return messageHandler(context, metadata.profileOnion, metadata.contactHandle, localIndex.index); + } + return Future.value(MalformedMessage(this.metadata)); + })), + key: Provider.of(bcontext).getMessageKey(idx)); + }); + } catch (e) { + return MalformedMessage(this.metadata).getWidget(context); + } + } +} diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart new file mode 100644 index 00000000..716c38ae --- /dev/null +++ b/lib/models/messages/textmessage.dart @@ -0,0 +1,37 @@ +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/messagebubble.dart'; +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../model.dart'; + +class TextMessage extends Message { + final MessageMetadata metadata; + final String content; + TextMessage(this.metadata, this.content); + + @override + Widget getPreviewWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + return MessageBubble(this.content); + }); + } + + @override + MessageMetadata getMetadata() { + return this.metadata; + } + + @override + Widget getWidget(BuildContext context) { + return ChangeNotifierProvider.value( + value: this.metadata, + builder: (bcontext, child) { + String idx = Provider.of(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString(); + return MessageRow(MessageBubble(this.content), key: Provider.of(bcontext).getMessageKey(idx)); + }); + } +} diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index c8d276b6..3c9abcf6 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:flutter/cupertino.dart'; @@ -108,24 +109,23 @@ class _MessageViewState extends State { if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { Provider.of(context) .cwtch - .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!).then((data) { - try { - var messageWrapper = jsonDecode(data! as String); - var bytes1 = utf8.encode(messageWrapper["PeerID"]+messageWrapper['Message']); - var digest1 = sha256.convert(bytes1); - var contentHash = base64Encode(digest1.bytes); - var quotedMessage = "{\"quotedHash\":\""+contentHash+"\",\"body\":\""+ctrlrCompose.value.text+"\"}"; - ChatMessage cm = new ChatMessage(o: 10, d: quotedMessage); - Provider.of(context, listen: false) - .cwtch - .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); - } catch (e) { - - } - Provider.of(context, listen: false).selectedIndex = null; - _sendMessageHelper(); + .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!) + .then((data) { + try { + var messageWrapper = jsonDecode(data! as String); + var bytes1 = utf8.encode(messageWrapper["PeerID"] + messageWrapper['Message']); + var digest1 = sha256.convert(bytes1); + var contentHash = base64Encode(digest1.bytes); + var quotedMessage = "{\"quotedHash\":\"" + contentHash + "\",\"body\":\"" + ctrlrCompose.value.text + "\"}"; + ChatMessage cm = new ChatMessage(o: 10, d: quotedMessage); + Provider.of(context, listen: false) + .cwtch + .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); + } catch (e) {} + Provider.of(context, listen: false).selectedIndex = null; + _sendMessageHelper(); }); - } else { + } else { ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); Provider.of(context, listen: false) .cwtch @@ -201,24 +201,17 @@ class _MessageViewState extends State { var children; if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { var quoted = FutureBuilder( - future: Provider.of(context) - .cwtch - .GetMessage(Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!), + future: messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!), builder: (context, snapshot) { if (snapshot.hasData) { - try { - var messageWrapper = jsonDecode(snapshot.data! as String); - dynamic message = jsonDecode(messageWrapper['Message']); - return Container( - margin: EdgeInsets.all(5), - padding: EdgeInsets.all(5), - color: messageWrapper['PeerID'] != Provider.of(context).selectedProfile - ? Provider.of(context).theme.messageFromOtherBackgroundColor() - : Provider.of(context).theme.messageFromMeBackgroundColor(), - child: Text(message["d"])); - } catch (e) { - return MalformedBubble(); - } + var message = snapshot.data! as Message; + return Container( + margin: EdgeInsets.all(5), + padding: EdgeInsets.all(5), + color: message.getMetadata().senderHandle != Provider.of(context).selectedProfile + ? Provider.of(context).theme.messageFromOtherBackgroundColor() + : Provider.of(context).theme.messageFromMeBackgroundColor(), + child: message.getPreviewWidget(context)); } else { return Text(""); } diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 47057f74..e1d91280 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -15,6 +16,12 @@ import 'messagebubbledecorations.dart'; // Like MessageBubble but for displaying chat overlay 100/101 invitations // Offers the user an accept/reject button if they don't have a matching contact already class InvitationBubble extends StatefulWidget { + final int overlay; + final String inviteTarget; + final String inviteNick; + + InvitationBubble(this.overlay, this.inviteTarget, this.inviteNick); + @override InvitationBubbleState createState() => InvitationBubbleState(); } @@ -25,32 +32,22 @@ class InvitationBubbleState extends State { @override Widget build(BuildContext context) { - if (Provider.of(context).malformed) { - return MalformedBubble(); - } - - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; - var isGroup = Provider.of(context).overlay == 101; - isAccepted = Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget) != null; - var prettyDate = ""; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; + var isGroup = widget.overlay == 101; + isAccepted = Provider.of(context).contactList.getContact(widget.inviteTarget) != null; var borderRadiousEh = 15.0; var showGroupInvite = Provider.of(context).isExperimentEnabled(TapirGroupsExperiment); - rejected = Provider.of(context).flags & 0x01 == 0x01; - var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); - - if (Provider.of(context).timestamp != null) { - // user-configurable timestamps prolly ideal? #todo - prettyDate = DateFormat.yMd().add_jm().format(Provider.of(context).timestamp); - } + rejected = Provider.of(context).flags & 0x01 == 0x01; + var prettyDate = DateFormat.yMd().add_jm().format(Provider.of(context).timestamp); // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; - if (!fromMe && Provider.of(context).senderOnion != null) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); + if (!fromMe) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { - senderDisplayStr = Provider.of(context).senderOnion; + senderDisplayStr = Provider.of(context).senderHandle; } } @@ -61,7 +58,7 @@ class InvitationBubbleState extends State { // If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from // some kind of malfeasance. - var selfInvite = Provider.of(context).inviteNick == Provider.of(context).onion; + var selfInvite = widget.inviteNick == Provider.of(context).onion; if (selfInvite) { return MalformedBubble(); } @@ -69,16 +66,15 @@ class InvitationBubbleState extends State { var wdgMessage = isGroup && !showGroupInvite ? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) : fromMe - ? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation, - isGroup ? Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget)!.nickname : Provider.of(context).message, myKey) - : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, Provider.of(context).inviteNick, - Provider.of(context).inviteTarget, myKey)); + ? senderInviteChrome( + AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of(context).contactList.getContact(widget.inviteTarget)!.nickname : widget.inviteTarget) + : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget)); Widget wdgDecorations; if (isGroup && !showGroupInvite) { wdgDecorations = Text('\u202F'); } else if (fromMe) { - wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); } else if (isAccepted) { wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F'); } else if (this.rejected) { @@ -131,22 +127,22 @@ class InvitationBubbleState extends State { setState(() { var profileOnion = Provider.of(context, listen: false).onion; var contact = Provider.of(context, listen: false).onion; - var idx = Provider.of(context, listen: false).messageIndex; - Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); - Provider.of(context).flags |= 0x01; + var idx = Provider.of(context, listen: false).messageIndex; + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); + Provider.of(context).flags |= 0x01; }); } void _btnAccept() { setState(() { var profileOnion = Provider.of(context, listen: false).onion; - Provider.of(context, listen: false).cwtch.ImportBundle(profileOnion, Provider.of(context, listen: false).message); + Provider.of(context, listen: false).cwtch.ImportBundle(profileOnion, widget.inviteTarget); isAccepted = true; }); } // Construct an invite chrome for the sender - Widget senderInviteChrome(String chrome, String targetName, String myKey) { + Widget senderInviteChrome(String chrome, String targetName) { return Wrap(children: [ SelectableText( chrome + '\u202F', @@ -159,7 +155,6 @@ class InvitationBubbleState extends State { ), SelectableText( targetName + '\u202F', - key: Key(myKey), style: TextStyle( color: Provider.of(context).theme.messageFromMeTextColor(), ), @@ -171,7 +166,7 @@ class InvitationBubbleState extends State { } // Construct an invite chrome - Widget inviteChrome(String chrome, String targetName, String targetId, String myKey) { + Widget inviteChrome(String chrome, String targetName, String targetId) { return Wrap(children: [ SelectableText( chrome + '\u202F', @@ -184,7 +179,6 @@ class InvitationBubbleState extends State { ), SelectableText( targetName + '\u202F', - key: Key(myKey), style: TextStyle(color: Provider.of(context).theme.messageFromOtherTextColor()), textAlign: TextAlign.left, maxLines: 2, diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index b6dde480..094e0ff9 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -1,3 +1,4 @@ +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -8,6 +9,10 @@ import '../settings.dart'; import 'messagebubbledecorations.dart'; class MessageBubble extends StatefulWidget { + final String content; + + MessageBubble(this.content); + @override MessageBubbleState createState() => MessageBubbleState(); } @@ -17,33 +22,30 @@ class MessageBubbleState extends State { @override Widget build(BuildContext context) { - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; var prettyDate = ""; var borderRadiousEh = 15.0; - var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); + // var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); - if (Provider.of(context).timestamp != null) { - // user-configurable timestamps prolly ideal? #todo - DateTime messageDate = Provider.of(context).timestamp; - prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); - } + DateTime messageDate = Provider.of(context).timestamp; + prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; - if (!fromMe && Provider.of(context).senderOnion != null) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); + if (!fromMe) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { - senderDisplayStr = Provider.of(context).senderOnion; + senderDisplayStr = Provider.of(context).senderHandle; } } var wdgSender = SelectableText(senderDisplayStr, style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); var wdgMessage = SelectableText( - (Provider.of(context).message ?? "") + '\u202F', - key: Key(myKey), + widget.content + '\u202F', + //key: Key(myKey), focusNode: _focus, style: TextStyle( color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), @@ -52,9 +54,9 @@ class MessageBubbleState extends State { textWidthBasis: TextWidthBasis.longestLine, ); - var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); - var error = Provider.of(context).error; + var error = Provider.of(context).error; return LayoutBuilder(builder: (context, constraints) { //print(constraints.toString()+", "+constraints.maxWidth.toString()); diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 5e985b7d..0ae041e6 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -1,7 +1,12 @@ +import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/messages/malformedmessage.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../main.dart'; import '../model.dart'; import '../settings.dart'; import 'messagerow.dart'; @@ -68,22 +73,22 @@ class _MessageListState extends State { itemCount: Provider.of(outerContext).totalMessages, reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... itemBuilder: (itemBuilderContext, index) { - var trueIndex = Provider.of(outerContext).totalMessages - index - 1; - return ChangeNotifierProvider( - key: ValueKey(trueIndex), - create: (x) => MessageState( - context: itemBuilderContext, - profileOnion: Provider.of(outerContext, listen: false).onion, - // We don't want to listen for updates to the contact handle... - contactHandle: Provider.of(x, listen: false).onion, - messageIndex: trueIndex, - ), - builder: (bcontext, child) { - String idx = Provider.of(outerContext).isGroup == true && Provider.of(bcontext).signature.isEmpty == false - ? Provider.of(bcontext).signature - : trueIndex.toString(); - return RepaintBoundary(child: MessageRow(key: Provider.of(bcontext).getMessageKey(idx))); - }); + var profileOnion = Provider.of(outerContext, listen: false).onion; + var contactHandle = Provider.of(outerContext, listen: false).onion; + var messageIndex = Provider.of(outerContext).totalMessages - index - 1; + + return FutureBuilder( + future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), + builder: (context, snapshot) { + if (snapshot.hasData) { + var message = snapshot.data as Message; + // Already includes MessageRow,, + return message.getWidget(context); + } else { + return MessageLoadingBubble(); + } + }, + ); }, ) : null))) diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index e37e7a85..29b7cfff 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:cwtch/widgets/quotedmessage.dart'; +import 'package:cwtch/models/message.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:provider/provider.dart'; @@ -9,36 +9,29 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; import '../model.dart'; import '../settings.dart'; -import 'invitationbubble.dart'; -import 'malformedbubble.dart'; -import 'messagebubble.dart'; -import 'messageloadingbubble.dart'; class MessageRow extends StatefulWidget { - MessageRow({Key? key}) : super(key: key); + final Widget child; + MessageRow(this.child, {Key? key}) : super(key: key); @override - _MessageRowState createState() => _MessageRowState(); + MessageRowState createState() => MessageRowState(); } -class _MessageRowState extends State { +class MessageRowState extends State { + bool showMenu = false; + @override Widget build(BuildContext context) { - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; - var malformed = Provider.of(context).malformed; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; - // If the message is malformed then override fromme as we can't trust it - if (malformed) { - fromMe = false; - } - - Widget wdgBubble = - Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of(context).loaded == true ? widgetForOverlay(Provider.of(context).overlay) : MessageLoadingBubble()); - Widget wdgIcons = IconButton( - onPressed: () { - Provider.of(context, listen: false).selectedIndex = Provider.of(context).messageIndex; - }, - icon: Icon(Icons.reply, color: Provider.of(context).theme.dropShadowColor())); + Widget wdgIcons = Visibility( + visible: this.showMenu, + child: IconButton( + onPressed: () { + Provider.of(context, listen: false).selectedIndex = Provider.of(context).messageIndex; + }, + icon: Icon(Icons.reply, color: Provider.of(context).theme.dropShadowColor()))); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); var widgetRow = []; @@ -46,7 +39,7 @@ class _MessageRowState extends State { widgetRow = [ wdgSpacer, wdgIcons, - wdgBubble, + Flexible(flex: 3, fit: FlexFit.loose, child: widget.child), ]; } else { var contact = Provider.of(context); @@ -56,7 +49,7 @@ class _MessageRowState extends State { padding: EdgeInsets.all(4.0), child: ProfileImage( diameter: 48.0, - imagePath: Provider.of(context).senderImage ?? contact.imagePath, + imagePath: Provider.of(context).senderImage ?? contact.imagePath, //maskOut: contact.status != "Authenticated", border: contact.status == "Authenticated" ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), badgeTextColor: Colors.red, badgeColor: Colors.red, @@ -64,30 +57,36 @@ class _MessageRowState extends State { widgetRow = [ wdgPortrait, - wdgBubble, + Flexible(flex: 3, fit: FlexFit.loose, child: widget.child), wdgIcons, wdgSpacer, ]; } - return Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow)); - } + return MouseRegion( + // For desktop... - Widget widgetForOverlay(int o) { - switch (o) { - case 1: - return MessageBubble(); - case 100: - case 101: - return InvitationBubble(); - case 10: - return QuotedMessageBubble(); - } - return MalformedBubble(); + onHover: (event) { + setState(() { + this.showMenu = true; + }); + }, + onExit: (event) { + setState(() { + this.showMenu = false; + }); + }, + child: GestureDetector( + + // Swipe to quote + onHorizontalDragEnd: (details) { + Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageIndex; + }, + child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow)))); } void _btnAdd() { - var sender = Provider.of(context, listen: false).senderOnion; + var sender = Provider.of(context, listen: false).senderHandle; if (sender == null || sender == "") { print("sender not yet loaded"); return; diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index e71d615d..74227242 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -1,7 +1,6 @@ -import 'dart:convert'; - -import 'package:cwtch/main.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; +import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../model.dart'; @@ -10,23 +9,12 @@ import 'package:intl/intl.dart'; import '../settings.dart'; import 'messagebubbledecorations.dart'; -class LocallyIndexedMessage { - final dynamic message; - final int index; - - LocallyIndexedMessage(this.message, this.index); - - LocallyIndexedMessage.fromJson(Map json) - : message = json['Message'], - index = json['LocalIndex']; - - Map toJson() => { - 'Message': message, - 'LocalIndex': index, - }; -} - class QuotedMessageBubble extends StatefulWidget { + final Future quotedMessage; + final String body; + + QuotedMessageBubble(this.body, this.quotedMessage); + @override QuotedMessageBubbleState createState() => QuotedMessageBubbleState(); } @@ -36,115 +24,85 @@ class QuotedMessageBubbleState extends State { @override Widget build(BuildContext context) { - var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; + var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; var prettyDate = ""; var borderRadiousEh = 15.0; - var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); - try { - dynamic message = jsonDecode(Provider.of(context).message); + DateTime messageDate = Provider.of(context).timestamp; + prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); - var quotedMessagePotentials = - Provider.of(context).cwtch.GetMessageByContentHash(Provider.of(context).profileOnion, Provider.of(context).contactHandle, message["quotedHash"]); - int messageIndex = Provider.of(context).messageIndex; - var quotedMessage = quotedMessagePotentials.then((matchingMessages) { - // reverse order the messages from newest to oldest and return the - // first matching message where it's index is less than the index of this - // message - try { - var list = (jsonDecode(matchingMessages) as List).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); - LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () { - return list.firstWhere((element) => messageIndex > element.index); - }); - return candidate; - } catch (e) { - // Malformed Message will be returned... - } - }); - - if (Provider.of(context).timestamp != null) { - // user-configurable timestamps prolly ideal? #todo - DateTime messageDate = Provider.of(context).timestamp; - prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal()); + // If the sender is not us, then we want to give them a nickname... + var senderDisplayStr = ""; + if (!fromMe) { + ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + if (contact != null) { + senderDisplayStr = contact.nickname; + } else { + senderDisplayStr = Provider.of(context).senderHandle; } - - // If the sender is not us, then we want to give them a nickname... - var senderDisplayStr = ""; - if (!fromMe && Provider.of(context).senderOnion != null) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderOnion); - if (contact != null) { - senderDisplayStr = contact.nickname; - } else { - senderDisplayStr = Provider.of(context).senderOnion; - } - } - var wdgSender = SelectableText(senderDisplayStr, - style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); - - var wdgMessage = SelectableText( - (message["body"] ?? "") + '\u202F', - key: Key(myKey), - focusNode: _focus, - style: TextStyle( - color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), - ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ); - - var wdgQuote = FutureBuilder( - future: quotedMessage, - builder: (context, snapshot) { - if (snapshot.hasData) { - var lim = (snapshot.data! as LocallyIndexedMessage); - var limmessage = lim.message; - // Swap the background color for quoted tweets.. - return Container( - margin: EdgeInsets.all(5), - padding: EdgeInsets.all(5), - color: fromMe ? Provider.of(context).theme.messageFromOtherBackgroundColor() : Provider.of(context).theme.messageFromMeBackgroundColor(), - child: Text(jsonDecode(limmessage)["d"])); - } else { - // This should be almost instantly resolved, any failure likely means an issue in decoding... - return MalformedBubble(); - } - }, - ); - - var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); - - var error = Provider.of(context).error; - - return LayoutBuilder(builder: (context, constraints) { - return RepaintBoundary( - child: Container( - child: Container( - decoration: BoxDecoration( - color: error - ? malformedColor - : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), - border: Border.all( - color: error - ? malformedColor - : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), - width: 1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(borderRadiousEh), - topRight: Radius.circular(borderRadiousEh), - bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, - bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), - ), - ), - child: Padding( - padding: EdgeInsets.all(9.0), - child: Column( - crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgQuote, wdgMessage, wdgDecorations] : [wdgSender, wdgQuote, wdgMessage, wdgDecorations]))))); - }); - } catch (e) { - return MalformedBubble(); } + var wdgSender = SelectableText(senderDisplayStr, + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); + + var wdgMessage = SelectableText( + widget.body + '\u202F', + focusNode: _focus, + style: TextStyle( + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + ); + + var wdgQuote = FutureBuilder( + future: widget.quotedMessage, + builder: (context, snapshot) { + if (snapshot.hasData) { + var qMessage = (snapshot.data! as Message); + // Swap the background color for quoted tweets.. + return Container( + margin: EdgeInsets.all(5), + padding: EdgeInsets.all(5), + color: fromMe ? Provider.of(context).theme.messageFromOtherBackgroundColor() : Provider.of(context).theme.messageFromMeBackgroundColor(), + child: qMessage.getPreviewWidget(context)); + } else { + // This should be almost instantly resolved, any failure likely means an issue in decoding... + return MessageLoadingBubble(); + } + }, + ); + + var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); + + var error = Provider.of(context).error; + + return LayoutBuilder(builder: (context, constraints) { + return RepaintBoundary( + child: Container( + child: Container( + decoration: BoxDecoration( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + border: Border.all( + color: error + ? malformedColor + : (fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor() : Provider.of(context).theme.messageFromOtherBackgroundColor()), + width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(borderRadiousEh), + topRight: Radius.circular(borderRadiousEh), + bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, + bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + ), + ), + child: Padding( + padding: EdgeInsets.all(9.0), + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgQuote, wdgMessage, wdgDecorations] : [wdgSender, wdgQuote, wdgMessage, wdgDecorations]))))); + }); } } diff --git a/pubspec.lock b/pubspec.lock index 2d7c5500..2ee3b476 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -375,7 +375,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" + version: "0.4.1" typed_data: dependency: transitive description: From 29c9c1615b7205380172fc8f3cb8514a1abd0436 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 7 Jul 2021 09:10:07 -0700 Subject: [PATCH 4/7] Upgrade libCwtch --- LIBCWTCH-GO.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 083b5514..d5ca9f05 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -v1.0.0-22-g343c3bc-2021-07-06-15-30 +v1.0.0-70-gafa6794-2021-07-07-01-24 \ No newline at end of file From e4046fb574d6e21b9f9a0a03710107eed0496ba9 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 7 Jul 2021 10:05:25 -0700 Subject: [PATCH 5/7] Addressing Dans comments + Marcia design --- LIBCWTCH-GO.version | 2 +- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 2 +- lib/l10n/intl_de.arb | 4 +- lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_es.arb | 4 +- lib/l10n/intl_fr.arb | 4 +- lib/l10n/intl_it.arb | 4 +- lib/l10n/intl_pl.arb | 4 +- lib/l10n/intl_pt.arb | 10 +--- lib/models/message.dart | 57 +++++++++++++------ lib/models/messages/invitemessage.dart | 5 +- lib/models/messages/quotedmessage.dart | 14 +++-- lib/models/messages/textmessage.dart | 3 +- lib/views/messageview.dart | 21 +++++-- lib/widgets/invitationbubble.dart | 2 +- lib/widgets/messagerow.dart | 1 + lib/widgets/quotedmessage.dart | 22 ++++--- 17 files changed, 109 insertions(+), 54 deletions(-) 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(); From 1c460b431ea75fa3c272b14d9d1043b9bc8ae2c0 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 7 Jul 2021 11:24:31 -0700 Subject: [PATCH 6/7] Remove print + add check for body in quote --- lib/models/messages/quotedmessage.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index d1210ff4..4d71a2d9 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -54,6 +54,11 @@ class QuotedMessage extends Message { Widget getWidget(BuildContext context) { try { dynamic message = jsonDecode(this.content); + + if (message["body"] == null || message["quotedHash"] == null) { + return MalformedMessage(this.metadata).getWidget(context); + } + var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]); int messageIndex = metadata.messageIndex; Future quotedMessage = quotedMessagePotentials.then((matchingMessages) { @@ -89,7 +94,6 @@ class QuotedMessage extends Message { key: Provider.of(bcontext).getMessageKey(idx)); }); } catch (e) { - print("Quoted message exception" + e.toString()); return MalformedMessage(this.metadata).getWidget(context); } } From 0c9da27363bc0616bb1212d6651e45c6dcfa4728 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 7 Jul 2021 11:31:16 -0700 Subject: [PATCH 7/7] const group handle length --- lib/models/message.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/models/message.dart b/lib/models/message.dart index de53c977..abd06828 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -19,6 +19,9 @@ const InviteGroupOverlay = 101; // it will likely be prudent to define a proper Contact wrapper. const TorV3ContactHandleLength = 56; +// Defines the length of a Cwtch v2 Group. +const GroupConversationHandleLength = 32; + abstract class Message { MessageMetadata getMetadata(); Widget getWidget(BuildContext context); @@ -55,7 +58,7 @@ Future messageHandler(BuildContext context, String profileOnion, String var error = messageWrapper['Error'] != null; String? signature; // If this is a group, store the signature - if (contactHandle.length == 32) { + if (contactHandle.length == GroupConversationHandleLength) { signature = messageWrapper['Signature']; } var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);