Merge pull request 'Implement Quoting' (#96) from quote into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #96
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
This commit is contained in:
Dan Ballard 2021-07-07 11:46:28 -07:00
commit 84a0ca5d19
28 changed files with 854 additions and 400 deletions

View File

@ -1 +1 @@
v1.0.0-22-g343c3bc-2021-07-06-15-30
v1.0.0-25-g801a805-2021-07-07-16-10

View File

@ -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.getMessagesByContentHash(profile, handle, contentHash)).build())
}
"UpdateMessageFlags" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""

View File

@ -32,6 +32,8 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index);
// ignore: non_constant_identifier_names
Future<dynamic> 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);

View File

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

View File

@ -57,9 +57,9 @@ typedef GetIntFromStrStrFn = int Function(Pointer<Utf8>, int, Pointer<Utf8>, int
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, 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<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, 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<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> 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<NativeFunction<get_json_blob_from_str_str_str_function>>("c_GetMessagesByContentHash");
// ignore: non_constant_identifier_names
final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrStrStrFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
final utf8contentHash = contentHash.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length);
String jsonMessage = jsonMessageBytes.toDartString();
return jsonMessage;
}
}

View File

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

View File

@ -1,6 +1,8 @@
{
"@@locale": "de",
"@@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": "Neue Nachricht in einer Gruppe!",
@ -42,16 +44,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.",

View File

@ -1,6 +1,8 @@
{
"@@locale": "en",
"@@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.",

View File

@ -1,6 +1,8 @@
{
"@@locale": "es",
"@@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.",

View File

@ -1,18 +1,20 @@
{
"@@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-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 !",
"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 +44,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 +62,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 +76,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 +90,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 +101,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 +125,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 +146,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 +188,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"
}

View File

@ -1,71 +1,67 @@
{
"@@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-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!",
"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",

View File

@ -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!",

View File

@ -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.",

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/messagebubble.dart';
@ -67,6 +68,7 @@ class AppState extends ChangeNotifier {
String appError = "";
String? _selectedProfile;
String? _selectedConversation;
int? _selectedIndex;
void SetCwtchInit() {
cwtchInit = true;
@ -90,6 +92,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;
}
@ -336,7 +344,7 @@ class ContactInfoState extends ChangeNotifier {
late int _unreadMessages = 0;
late int _totalMessages = 0;
late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageBubbleState>> keys;
late Map<String, GlobalKey<MessageRowState>> keys;
// todo: a nicer way to model contacts, groups and other "entities"
late bool _isGroup;
@ -368,7 +376,7 @@ class ContactInfoState extends ChangeNotifier {
this._savePeerHistory = savePeerHistory;
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server;
keys = Map<String, GlobalKey<MessageBubbleState>>();
keys = Map<String, GlobalKey<MessageRowState>>();
}
String get nickname => this._nickname;
@ -444,137 +452,11 @@ class ContactInfoState extends ChangeNotifier {
}
}
GlobalKey<MessageBubbleState> getMessageKey(String index) {
GlobalKey<MessageRowState> getMessageKey(String index) {
if (keys[index] == null) {
keys[index] = GlobalKey<MessageBubbleState>();
keys[index] = GlobalKey<MessageRowState>();
}
GlobalKey<MessageBubbleState> ret = keys[index]!;
GlobalKey<MessageRowState> ret = keys[index]!;
return ret;
}
}
class MessageState extends ChangeNotifier {
final String profileOnion;
final String contactHandle;
final int messageIndex;
late String _message;
late int _overlay;
late String _inviteTarget;
late String _inviteNick;
late DateTime _timestamp;
late String _senderOnion;
late int _flags;
String? _senderImage;
late String _signature = "";
late bool _ackd = false;
late bool _error = false;
late bool _loaded = false;
late bool _malformed = false;
MessageState({
required BuildContext context,
required this.profileOnion,
required this.contactHandle,
required this.messageIndex,
}) {
this._senderOnion = profileOnion;
tryLoad(context);
}
get message => this._message;
get overlay => this._overlay;
get timestamp => this._timestamp;
int get flags => this._flags;
set flags(int newVal) {
this._flags = newVal;
notifyListeners();
}
bool get ackd => this._ackd;
bool get error => this._error;
bool get malformed => this._malformed;
bool get loaded => this._loaded;
get senderOnion => this._senderOnion;
get senderImage => this._senderImage;
get signature => this._signature;
get isInvite => this.overlay == 100 || this.overlay == 101;
get inviteTarget => this._inviteTarget;
get inviteNick => this._inviteNick;
set ackd(bool newVal) {
this._ackd = newVal;
notifyListeners();
}
set error(bool newVal) {
this._error = newVal;
notifyListeners();
}
set malformed(bool newVal) {
this._malformed = newVal;
notifyListeners();
}
set loaded(bool newVal) {
// quickly-arriving messages get discarded before loading sometimes
if (!hasListeners) return;
this._loaded = newVal;
notifyListeners();
}
void tryLoad(BuildContext context) {
Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) {
try {
dynamic messageWrapper = jsonDecode(jsonMessage);
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
this._senderOnion = profileOnion;
Future.delayed(const Duration(milliseconds: 2), () {
tryLoad(context);
});
return;
}
dynamic message = jsonDecode(messageWrapper['Message']);
this._message = message['d'];
this._overlay = int.parse(message['o'].toString());
this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
this._senderOnion = messageWrapper['PeerID'];
this._senderImage = messageWrapper['ContactImage'];
this._flags = int.parse(messageWrapper['Flags'].toString(), radix: 2);
// If this is a group, store the signature
if (contactHandle.length == 32) {
this._signature = messageWrapper['Signature'];
}
// if this is an invite, get the contact handle
if (this.isInvite) {
if (message['d'].toString().length == 56) {
this._inviteTarget = message['d'];
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(this._inviteTarget);
this._inviteNick = targetContact == null ? message['d'] : targetContact.nickname;
} else {
var parts = message['d'].toString().split("||");
if (parts.length == 2) {
var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5)));
this._inviteTarget = jsonObj['GroupID'];
this._inviteNick = jsonObj['GroupName'];
}
}
}
this.loaded = true;
//update ackd and error last as they are changenotified
this.ackd = messageWrapper['Acknowledged'];
if (messageWrapper['Error'] != null) {
this.error = true;
}
} catch (e) {
this._overlay = -1;
this.loaded = true;
this.malformed = true;
}
});
}
}

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

@ -0,0 +1,126 @@
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';
// 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;
// Defines the length of a Cwtch v2 Group.
const GroupConversationHandleLength = 32;
abstract class Message {
MessageMetadata getMetadata();
Widget getWidget(BuildContext context);
Widget getPreviewWidget(BuildContext context);
}
Future<Message> messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) {
try {
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index);
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
// 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);
});
}
// 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 == GroupConversationHandleLength) {
signature = messageWrapper['Signature'];
}
var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);
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) {
return Future.value(MalformedMessage(MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true)));
}
}
class MessageMetadata extends ChangeNotifier {
// meta-metadata
final String profileOnion;
final String contactHandle;
final int messageIndex;
final DateTime timestamp;
final String senderHandle;
final String? senderImage;
int _flags;
bool _ackd;
bool _error;
final String? signature;
int get flags => this._flags;
set flags(int newVal) {
this._flags = newVal;
notifyListeners();
}
bool get ackd => this._ackd;
set ackd(bool newVal) {
this._ackd = newVal;
notifyListeners();
}
bool get error => this._error;
set error(bool newVal) {
this._error = newVal;
notifyListeners();
}
MessageMetadata(this.profileOnion, this.contactHandle, this.messageIndex, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error);
}

View File

@ -0,0 +1,77 @@
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/messagerow.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../../model.dart';
class InviteMessage extends Message {
final MessageMetadata metadata;
final String content;
final int overlay;
InviteMessage(this.overlay, this.metadata, this.content);
@override
Widget getWidget(BuildContext context) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
String inviteTarget;
String inviteNick;
if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else {
var parts = this.content.toString().split("||");
if (parts.length == 2) {
var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5)));
inviteTarget = jsonObj['GroupID'];
inviteNick = jsonObj['GroupName'];
} else {
return MessageRow(MalformedBubble());
}
}
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
});
}
@override
Widget getPreviewWidget(BuildContext context) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String inviteTarget;
String inviteNick;
if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else {
var parts = this.content.toString().split("||");
if (parts.length == 2) {
var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5)));
inviteTarget = jsonObj['GroupID'];
inviteNick = jsonObj['GroupName'];
} else {
return MalformedBubble();
}
}
return InvitationBubble(overlay, inviteTarget, inviteNick);
});
}
@override
MessageMetadata getMetadata() {
return this.metadata;
}
}

View File

@ -0,0 +1,33 @@
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class MalformedMessage extends Message {
final MessageMetadata metadata;
MalformedMessage(this.metadata);
@override
Widget getWidget(BuildContext context) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (context, child) {
return MessageRow(MalformedBubble());
});
}
@override
Widget getPreviewWidget(BuildContext context) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
return MalformedBubble();
});
}
@override
MessageMetadata getMetadata() {
return this.metadata;
}
}

View File

@ -0,0 +1,100 @@
import 'dart:convert';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:cwtch/widgets/quotedmessage.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../../main.dart';
import '../../model.dart';
class LocallyIndexedMessage {
final dynamic message;
final int index;
LocallyIndexedMessage(this.message, this.index);
LocallyIndexedMessage.fromJson(Map<String, dynamic> json)
: message = json['Message'],
index = json['LocalIndex'];
Map<String, dynamic> toJson() => {
'Message': message,
'LocalIndex': index,
};
}
class QuotedMessage extends Message {
final MessageMetadata metadata;
final String content;
QuotedMessage(this.metadata, this.content);
@override
Widget getPreviewWidget(BuildContext context) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
try {
dynamic message = jsonDecode(this.content);
return Text(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);
if (message["body"] == null || message["quotedHash"] == null) {
return MalformedMessage(this.metadata).getWidget(context);
}
var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]);
int messageIndex = metadata.messageIndex;
Future<LocallyIndexedMessage?> 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
try {
var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList();
LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () {
return list.firstWhere((element) => messageIndex > element.index);
});
return candidate;
} catch (e) {
// Malformed Message will be returned...
return null;
}
});
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
return MessageRow(
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
if (localIndex != null) {
return messageHandler(context, metadata.profileOnion, metadata.contactHandle, localIndex.index);
}
return MalformedMessage(this.metadata);
})),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
});
} catch (e) {
return MalformedMessage(this.metadata).getWidget(context);
}
}
}

View File

@ -0,0 +1,38 @@
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 Text(this.content);
});
}
@override
MessageMetadata getMetadata() {
return this.metadata;
}
@override
Widget getWidget(BuildContext context) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
return MessageRow(MessageBubble(this.content), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
});
}
}

View File

@ -1,7 +1,10 @@
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/messageloadingbubble.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -104,11 +107,32 @@ class _MessageViewState extends State<MessageView> {
void _sendMessage([String? ignoredParam]) {
if (ctrlrCompose.value.text.isNotEmpty) {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
_sendMessageHelper();
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
Provider.of<FlwtchState>(context)
.cwtch
.GetMessage(Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!)
.then((data) {
try {
var messageWrapper = jsonDecode(data! as String);
var bytes1 = utf8.encode(messageWrapper["PeerID"] + messageWrapper['Message']);
var digest1 = sha256.convert(bytes1);
var contentHash = base64Encode(digest1.bytes);
var quotedMessage = "{\"quotedHash\":\"" + contentHash + "\",\"body\":\"" + ctrlrCompose.value.text + "\"}";
ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
} catch (e) {}
Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper();
});
} else {
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
_sendMessageHelper();
}
}
}
@ -130,7 +154,7 @@ class _MessageViewState extends State<MessageView> {
}
Widget _buildComposeBox() {
return Container(
var composeBox = Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor(),
padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2),
@ -141,52 +165,88 @@ class _MessageViewState extends State<MessageView> {
child: Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(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<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendInvite,
enableFeedback: true,
splashColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
hoverColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
onPressed: () => _modalSendInvitation(context)),
suffixIcon: IconButton(
icon: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(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<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendInvite,
enableFeedback: true,
splashColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
hoverColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
onPressed: () => _modalSendInvitation(context)),
suffixIcon: IconButton(
icon: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendMessage,
onPressed: _sendMessage,
),
)))),
),
],
),
);
var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
var quoted = FutureBuilder(
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!),
builder: (context, snapshot) {
if (snapshot.hasData) {
var message = snapshot.data! as Message;
return Container(
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
child: 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<AppState>(context, listen: false).selectedIndex = null;
},
))
]));
} else {
return MessageLoadingBubble();
}
},
);
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() => {};

View File

@ -98,6 +98,7 @@ class _ContactRowState extends State<ContactRow> {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).selectedConversation = contact.onion;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion);

View File

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

View File

@ -1,3 +1,4 @@
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -8,6 +9,10 @@ import '../settings.dart';
import 'messagebubbledecorations.dart';
class MessageBubble extends StatefulWidget {
final String content;
MessageBubble(this.content);
@override
MessageBubbleState createState() => MessageBubbleState();
}
@ -17,33 +22,30 @@ class MessageBubbleState extends State<MessageBubble> {
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var prettyDate = "";
var borderRadiousEh = 15.0;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
// var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
if (Provider.of<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo
DateTime messageDate = Provider.of<MessageState>(context).timestamp;
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
}
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText(
(Provider.of<MessageState>(context).message ?? "") + '\u202F',
key: Key(myKey),
widget.content + '\u202F',
//key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
@ -52,9 +54,9 @@ class MessageBubbleState extends State<MessageBubble> {
textWidthBasis: TextWidthBasis.longestLine,
);
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var error = Provider.of<MessageState>(context).error;
var error = Provider.of<MessageMetadata>(context).error;
return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString());

View File

@ -1,7 +1,12 @@
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'messagerow.dart';
@ -68,22 +73,22 @@ class _MessageListState extends State<MessageList> {
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
itemBuilder: (itemBuilderContext, index) {
var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
return ChangeNotifierProvider(
key: ValueKey(trueIndex),
create: (x) => MessageState(
context: itemBuilderContext,
profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
// We don't want to listen for updates to the contact handle...
contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion,
messageIndex: trueIndex,
),
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false
? Provider.of<MessageState>(bcontext).signature
: trueIndex.toString();
return RepaintBoundary(child: MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)));
});
var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion;
var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
return FutureBuilder(
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
builder: (context, snapshot) {
if (snapshot.hasData) {
var message = snapshot.data as Message;
// Already includes MessageRow,,
return message.getWidget(context);
} else {
return MessageLoadingBubble();
}
},
);
},
)
: null)))

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cwtch/models/message.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:provider/provider.dart';
@ -8,40 +9,38 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'invitationbubble.dart';
import 'malformedbubble.dart';
import 'messagebubble.dart';
import 'messageloadingbubble.dart';
class MessageRow extends StatefulWidget {
MessageRow({Key? key}) : super(key: key);
final Widget child;
MessageRow(this.child, {Key? key}) : super(key: key);
@override
_MessageRowState createState() => _MessageRowState();
MessageRowState createState() => MessageRowState();
}
class _MessageRowState extends State<MessageRow> {
class MessageRowState extends State<MessageRow> {
bool showMenu = false;
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var malformed = Provider.of<MessageState>(context).malformed;
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
// If the message is malformed then override fromme as we can't trust it
if (malformed) {
fromMe = false;
}
Widget wdgBubble =
Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble());
Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of<Settings>(context).theme.dropShadowColor());
Widget wdgIcons = Visibility(
visible: this.showMenu,
child: IconButton(
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context).messageIndex;
},
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[];
if (fromMe) {
widgetRow = <Widget>[
wdgSpacer,
//wdgIcons,
wdgBubble,
wdgIcons,
Flexible(flex: 3, fit: FlexFit.loose, child: widget.child),
];
} else {
var contact = Provider.of<ContactInfoState>(context);
@ -51,7 +50,7 @@ class _MessageRowState extends State<MessageRow> {
padding: EdgeInsets.all(4.0),
child: ProfileImage(
diameter: 48.0,
imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath,
imagePath: Provider.of<MessageMetadata>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
badgeTextColor: Colors.red, badgeColor: Colors.red,
@ -59,28 +58,36 @@ class _MessageRowState extends State<MessageRow> {
widgetRow = <Widget>[
wdgPortrait,
wdgBubble,
//wdgIcons,
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();
}
return MalformedBubble();
onHover: (event) {
setState(() {
this.showMenu = true;
});
},
onExit: (event) {
setState(() {
this.showMenu = false;
});
},
child: GestureDetector(
// Swipe to quote
onHorizontalDragEnd: (details) {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
},
child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow))));
}
void _btnAdd() {
var sender = Provider.of<MessageState>(context, listen: false).senderOnion;
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") {
print("sender not yet loaded");
return;

View File

@ -0,0 +1,116 @@
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';
import 'package:intl/intl.dart';
import '../settings.dart';
import 'messagebubbledecorations.dart';
class QuotedMessageBubble extends StatefulWidget {
final Future<Message> quotedMessage;
final String body;
QuotedMessageBubble(this.body, this.quotedMessage);
@override
QuotedMessageBubbleState createState() => QuotedMessageBubbleState();
}
class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
FocusNode _focus = FocusNode();
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var prettyDate = "";
var borderRadiousEh = 15.0;
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = "";
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText(
widget.body + '\u202F',
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
var wdgQuote = FutureBuilder(
future: widget.quotedMessage,
builder: (context, snapshot) {
if (snapshot.hasData) {
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<Settings>(context).theme.messageFromOtherBackgroundColor() : Provider.of<Settings>(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();
}
},
);
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
var error = Provider.of<MessageMetadata>(context).error;
return LayoutBuilder(builder: (context, constraints) {
return RepaintBoundary(
child: Container(
child: Container(
decoration: BoxDecoration(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
border: Border.all(
color: error
? malformedColor
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()),
width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
),
),
child: Padding(
padding: EdgeInsets.all(9.0),
child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgQuote, wdgMessage, wdgDecorations] : [wdgSender, wdgQuote, wdgMessage, wdgDecorations])))));
});
}
}

View File

@ -42,7 +42,7 @@ packages:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.1"
clock:
dependency: transitive
description:
@ -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:
@ -368,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:

View File

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