Merge pull request 'Implement Quoting' (#96) from quote into trunk
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #96 Reviewed-by: Dan Ballard <dan@openprivacy.ca>
This commit is contained in:
commit
84a0ca5d19
|
@ -1 +1 @@
|
|||
v1.0.0-22-g343c3bc-2021-07-06-15-30
|
||||
v1.0.0-25-g801a805-2021-07-07-16-10
|
|
@ -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) ?: ""
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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!",
|
||||
|
|
|
@ -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.",
|
||||
|
|
144
lib/model.dart
144
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';
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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() => {};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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])))));
|
||||
});
|
||||
}
|
||||
}
|
11
pubspec.lock
11
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:
|
||||
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue