From 267b1b09b19e0fe9679a4c2a59eff22aff81a3f3 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 4 Apr 2023 13:58:42 -0700 Subject: [PATCH 1/4] Status + Profile Attributes --- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 11 ++ lib/cwtch/cwtch.dart | 3 + lib/cwtch/cwtchNotifier.dart | 47 ++++++- lib/cwtch/ffi.dart | 53 ++++++++ lib/cwtch/gomobile.dart | 18 +++ lib/l10n/intl_cy.arb | 9 +- lib/l10n/intl_da.arb | 9 +- lib/l10n/intl_de.arb | 9 +- lib/l10n/intl_el.arb | 9 +- lib/l10n/intl_en.arb | 9 +- lib/l10n/intl_es.arb | 9 +- lib/l10n/intl_fr.arb | 9 +- lib/l10n/intl_it.arb | 9 +- lib/l10n/intl_ko.arb | 19 ++- lib/l10n/intl_lb.arb | 9 +- lib/l10n/intl_nl.arb | 9 +- lib/l10n/intl_no.arb | 9 +- lib/l10n/intl_pl.arb | 9 +- lib/l10n/intl_pt.arb | 9 +- lib/l10n/intl_pt_BR.arb | 9 +- lib/l10n/intl_ro.arb | 9 +- lib/l10n/intl_ru.arb | 9 +- lib/l10n/intl_sk.arb | 9 +- lib/l10n/intl_tr.arb | 9 +- lib/main.dart | 8 +- lib/models/contact.dart | 63 ++++++++- lib/models/messages/filemessage.dart | 10 +- lib/models/profile.dart | 39 ++++++ lib/themes/opaque.dart | 3 + lib/views/addeditprofileview.dart | 124 +++++++++++++----- lib/views/contactsview.dart | 39 +++++- lib/views/messageview.dart | 19 +-- lib/views/peersettingsview.dart | 22 +++- lib/widgets/contactrow.dart | 14 +- lib/widgets/messagerow.dart | 2 +- lib/widgets/textfield.dart | 1 + 36 files changed, 561 insertions(+), 96 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 0065c815..3b3c4d85 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -483,6 +483,17 @@ class MainActivity: FlutterActivity() { val v: String = call.argument("Val") ?: "" Cwtch.setProfileAttribute(profile, key, v) } + "GetProfileAttribute" -> { + val profile: String = call.argument("ProfileOnion") ?: "" + val key: String = call.argument("Key") ?: "" + Data.Builder().putString("result", Cwtch.getProfileAttribute(profile, key)).build() + } + "GetConversationAttribute" -> { + val profile: String = call.argument("ProfileOnion") ?: "" + val conversation: Int = call.argument("conversation") ?: 0 + val key: String = call.argument("Key") ?: "" + Data.Builder().putString("result", Cwtch.getConversationAttribute(profile, conversation.toLong(), key)).build() + } "SetConversationAttribute" -> { val profile: String = call.argument("ProfileOnion") ?: "" val conversation: Int = call.argument("conversation") ?: 0 diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 6dbb6f29..394926c5 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -95,9 +95,12 @@ abstract class Cwtch { Future ImportBundle(String profile, String bundle); // ignore: non_constant_identifier_names void SetProfileAttribute(String profile, String key, String val); + String? GetProfileAttribute(String profile, String key); // ignore: non_constant_identifier_names void SetConversationAttribute(String profile, int conversation, String key, String val); // ignore: non_constant_identifier_names + String? GetConversationAttribute(String profile, int identifier, String s); + // ignore: non_constant_identifier_names void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val); // ignore: non_constant_identifier_names void LoadServers(String password); diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index ca268689..85324657 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -30,14 +30,15 @@ class CwtchNotifier { late NotificationsManager notificationManager; late AppState appState; late ServerListState serverListState; + late FlwtchState flwtchState; String? notificationSimple; String? notificationConversationInfo; SeenMessageCallback? seenMessageCallback; - CwtchNotifier( - ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) { + CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, + ServerListState serverListStateCN, FlwtchState flwtchStateCN) { profileCN = pcn; settings = settingsCN; error = errorCN; @@ -45,6 +46,7 @@ class CwtchNotifier { notificationManager = notificationManagerP; appState = appStateCN; serverListState = serverListStateCN; + flwtchState = flwtchStateCN; } void l10nInit(String notificationSimple, String notificationConversationInfo) { @@ -74,6 +76,19 @@ class CwtchNotifier { // if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta... profileCN.add(data["Identity"], data["name"], data["picture"], data["defaultPicture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["autostart"] == "true", data["tag"] != "v1-defaultPassword"); + + // Update Profile Attributes + profileCN.getProfile(data["Identity"])?.setAttribute(0, flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-attribute-1")); + profileCN.getProfile(data["Identity"])?.setAttribute(1, flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-attribute-2")); + profileCN.getProfile(data["Identity"])?.setAttribute(2, flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-attribute-3")); + profileCN.getProfile(data["Identity"])?.setAvailabilityStatus(flwtchState.cwtch.GetProfileAttribute(data["Identity"], "profile.profile-status") ?? ""); + profileCN.getProfile(data["Identity"])?.contactList.contacts.forEach((contact) { + contact.setAttribute(0, flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-attribute-1")); + contact.setAttribute(1, flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-attribute-2")); + contact.setAttribute(2, flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-attribute-3")); + contact.setAvailabilityStatus(flwtchState.cwtch.GetConversationAttribute(data["Identity"], contact.identifier, "public.profile.profile-status") ?? ""); + }); + break; case "ContactCreated": EnvironmentConfig.debugLog("ContactCreated $data"); @@ -291,6 +306,10 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.downloadSetPathForSender(filekey, data["Data"]); } } + } else if (data["Key"].toString().startsWith("public.profile.profile-attribute")) { + // ignore these events... + } else if (data["Key"].toString().startsWith("public.profile.profile-status")) { + profileCN.getProfile(data["ProfileOnion"])?.setAvailabilityStatus(data["Data"]); } else { EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}"); } @@ -377,6 +396,30 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.waitForDownloadComplete(contact.identifier, fileKey); } } + } else if (data['Path'] == "profile.profile-attribute-1" || data['Path'] == "profile.profile-attribute-2" || data['Path'] == "profile.profile-attribute-3") { + if (data["Exists"] == "true") { + var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]); + if (contact != null) { + switch (data['Path']) { + case "profile.profile-attribute-1": + contact.setAttribute(0, data["Data"]); + break; + case "profile.profile-attribute-2": + contact.setAttribute(1, data["Data"]); + break; + case "profile.profile-attribute-3": + contact.setAttribute(2, data["Data"]); + break; + } + } + } + } else if (data['Path'] == "profile.profile-status") { + if (data["Exists"] == "true") { + var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]); + if (contact != null) { + contact.setAvailabilityStatus(data['Data']); + } + } } else { EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}"); } diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 8b601eed..b5ed651c 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -72,6 +72,9 @@ typedef GetJsonBlobFromStrStrIntFn = Pointer Function(Pointer, int, typedef get_json_blob_from_str_int_function = Pointer Function(Pointer, Int32, Int32); typedef GetJsonBlobFromStrIntFn = Pointer Function(Pointer, int, int); +typedef get_json_blob_from_str_str_function = Pointer Function(Pointer, Int32, Pointer, Int32); +typedef GetJsonBlobFromStrStrFn = Pointer Function(Pointer, int, Pointer, int); + typedef get_json_blob_from_str_int_int_str_function = Pointer Function(Pointer, Int32, Int32, Int32, Pointer, Int32); typedef GetJsonBlobFromStrIntIntStrFn = Pointer Function( Pointer, @@ -962,4 +965,54 @@ class CwtchFfi implements Cwtch { } return false; } + + @override + String? GetProfileAttribute(String profile, String key) { + var getProfileAttributeC = library.lookup>("c_GetProfileAttribute"); + // ignore: non_constant_identifier_names + final GetProfileAttribute = getProfileAttributeC.asFunction(); + final utf8profile = profile.toNativeUtf8(); + final utf8key = key.toNativeUtf8(); + Pointer jsonMessageBytes = GetProfileAttribute(utf8profile, utf8profile.length, utf8key, utf8key.length); + String jsonMessage = jsonMessageBytes.toDartString(); + _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); + malloc.free(utf8profile); + malloc.free(utf8key); + + try { + dynamic attributeResult = json.decode(jsonMessage); + if (attributeResult["Exists"]) { + return attributeResult["Value"]; + } + } catch (e) { + EnvironmentConfig.debugLog("error getting profile attribute: $e"); + } + + return null; + } + + @override + String? GetConversationAttribute(String profile, int conversation, String key) { + var getConversationAttributeC = library.lookup>("c_GetConversationAttribute"); + // ignore: non_constant_identifier_names + final GetConversationAttribute = getConversationAttributeC.asFunction(); + final utf8profile = profile.toNativeUtf8(); + final utf8key = key.toNativeUtf8(); + Pointer jsonMessageBytes = GetConversationAttribute(utf8profile, utf8profile.length, conversation, utf8key, utf8key.length); + String jsonMessage = jsonMessageBytes.toDartString(); + _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); + malloc.free(utf8profile); + malloc.free(utf8key); + + try { + dynamic attributeResult = json.decode(jsonMessage); + if (attributeResult["Exists"]) { + return attributeResult["Value"]; + } + } catch (e) { + EnvironmentConfig.debugLog("error getting profile attribute: $e"); + } + + return null; + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 78d56b5c..7bbdc12f 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -388,4 +388,22 @@ class CwtchGomobile implements Cwtch { // Blodeuwedd is not currently supported on lower end devices. return false; } + + @override + String? GetProfileAttribute(String profile, String key) { + dynamic attributeResult = cwtchPlatform.invokeMethod("GetProfileAttribute", {"ProfileOnion": profile, "key": key}); + if (attributeResult["Exists"]) { + return attributeResult["Value"]; + } + return null; + } + + @override + String? GetConversationAttribute(String profile, int conversation, String key) { + dynamic attributeResult = cwtchPlatform.invokeMethod("GetProfileAttribute", {"ProfileOnion": profile, "conversation": conversation, "key": key}); + if (attributeResult["Exists"]) { + return attributeResult["Value"]; + } + return null; + } } diff --git a/lib/l10n/intl_cy.arb b/lib/l10n/intl_cy.arb index 998c8fa1..e276526d 100644 --- a/lib/l10n/intl_cy.arb +++ b/lib/l10n/intl_cy.arb @@ -1,6 +1,13 @@ { "@@locale": "cy", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_da.arb b/lib/l10n/intl_da.arb index 0eb5d4a8..3d964a6a 100644 --- a/lib/l10n/intl_da.arb +++ b/lib/l10n/intl_da.arb @@ -1,6 +1,13 @@ { "@@locale": "da", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 6b07ef5c..eb2843fd 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,13 @@ { "@@locale": "de", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_el.arb b/lib/l10n/intl_el.arb index d7662a9b..342f1f09 100644 --- a/lib/l10n/intl_el.arb +++ b/lib/l10n/intl_el.arb @@ -1,6 +1,13 @@ { "@@locale": "el", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ed694fe9..af12bda1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,13 @@ { "@@locale": "en", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusAway": "Away", + "availabilityStatusBusy": "Busy", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddSummarize": "Summarize Conversation", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 54c5c3d2..e81c4531 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,13 @@ { "@@locale": "es", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 25cb0f8a..c8c88e1f 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,13 @@ { "@@locale": "fr", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 7dfc262c..8ded0001 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,13 @@ { "@@locale": "it", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb index a328e7dc..c4bbe2e2 100644 --- a/lib/l10n/intl_ko.arb +++ b/lib/l10n/intl_ko.arb @@ -1,14 +1,21 @@ { "@@locale": "ko", - "@@last_modified": "2023-03-27T20:38:31+02:00", - "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", - "blodeuweddProcessing": "Blodeuwedd is processing...", - "blodeuweddTranslate": "Translate Message", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", + "blodeuweddExperimentEnable": "Blodeuwedd (블러드웨드) 어시스턴트", + "blodeuweddDescription": "Blodeuwedd (불러드웨드) 어시스턴트는 로컬에서 호스팅되는 언어 모델을 통한 대화 내용 요약 및 메시지 번역과 같은 Cwtch에 새로운 기능을 추가합니다.", "blodeuweddSummarize": "Summarize Conversation", + "blodeuweddTranslate": "메시지 번역", + "blodeuweddProcessing": "Blodeuwedd 처리 중...", + "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddPath": "The directory where the Blodeuwedd is located on your computer.", "blodeuweddNotSupported": "This version of Cwtch has been compiled without support for the Blodeuwedd Assistant.", - "blodeuweddDescription": "The Blodeuwedd assistant adds new features to Cwtch such as chat transcript summarization and message translation via a locally hosted language model.", - "blodeuweddExperimentEnable": "Blodeuwedd Assistant", "labelACNCircuitInfo": "ACN 회로 정보", "clickableLinksWarning": "이 URL을 열면 Cwtch 외부에서 응용 프로그램이 시작되고 메타데이터가 노출되거나 Cwtch의 보안이 손상될 수 있습니다. 신뢰할 수 있는 사용자의 URL만 엽니다. 계속하시겠습니까?", "thisFeatureRequiresGroupExpermientsToBeEnabled": "이 기능을 사용하려면 설정에서 그룹 실험을 사용하도록 설정해야 합니다.", diff --git a/lib/l10n/intl_lb.arb b/lib/l10n/intl_lb.arb index 7861d0b8..0e5b2af9 100644 --- a/lib/l10n/intl_lb.arb +++ b/lib/l10n/intl_lb.arb @@ -1,6 +1,13 @@ { "@@locale": "lb", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_nl.arb b/lib/l10n/intl_nl.arb index 19ff7488..c63e98ea 100644 --- a/lib/l10n/intl_nl.arb +++ b/lib/l10n/intl_nl.arb @@ -1,6 +1,13 @@ { "@@locale": "nl", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_no.arb b/lib/l10n/intl_no.arb index 346146ed..007e3153 100644 --- a/lib/l10n/intl_no.arb +++ b/lib/l10n/intl_no.arb @@ -1,6 +1,13 @@ { "@@locale": "no", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 339aa25f..66f79eee 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,13 @@ { "@@locale": "pl", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 2bb91d9d..0a3ad93d 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,13 @@ { "@@locale": "pt", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_pt_BR.arb b/lib/l10n/intl_pt_BR.arb index 809899dd..a4452a04 100644 --- a/lib/l10n/intl_pt_BR.arb +++ b/lib/l10n/intl_pt_BR.arb @@ -1,6 +1,13 @@ { "@@locale": "pt_BR", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index dfc274e6..a0980398 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -1,6 +1,13 @@ { "@@locale": "ro", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 70375987..3919496f 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,13 @@ { "@@locale": "ru", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_sk.arb b/lib/l10n/intl_sk.arb index 15806e78..931c86ea 100644 --- a/lib/l10n/intl_sk.arb +++ b/lib/l10n/intl_sk.arb @@ -1,6 +1,13 @@ { "@@locale": "sk", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/l10n/intl_tr.arb b/lib/l10n/intl_tr.arb index 1f806ed4..29fe687d 100644 --- a/lib/l10n/intl_tr.arb +++ b/lib/l10n/intl_tr.arb @@ -1,6 +1,13 @@ { "@@locale": "tr", - "@@last_modified": "2023-03-27T20:38:31+02:00", + "@@last_modified": "2023-04-03T22:39:52+02:00", + "profileInfoHint3": "Contacts will be able to see this information in Conversation Settings ", + "profileInfoHint2": "You can add up to 3 fields.", + "profileInfoHint": "Add some public information about yourself here e.g. blog, websites, brief bio.", + "availabilityStatusTooltip": "Set your availability status.", + "availabilityStatusBusy": "Busy", + "availabilityStatusAway": "Away", + "availabilityStatusAvailable": "Available", "blodeuweddWarning": "Blodeuwedd uses a local language model and a set of small auxiliary models to power its functionality. These techniques are often very effective they are not without error. \n\nWhile we have taken efforts to minimize the risk, there is still the possibility that Blodeuwedd outputs will be incorrect, hallucinated and\/or offensive.\n\nBecause of that Blodeuwedd requires downloading two additional components separate from Cwtch, the Blodeuwedd Model (or a compatible model) and the Blodeuwedd Runner. \n\nSee https:\/\/docs.cwtch.im\/docs\/settings\/experiments\/blodeuwedd for more information on obtaining these components and setting them up.", "blodeuweddProcessing": "Blodeuwedd is processing...", "blodeuweddTranslate": "Translate Message", diff --git a/lib/main.dart b/lib/main.dart index 180ffae7..f972a018 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -88,13 +88,15 @@ class FlwtchState extends State with WindowListener { shutdownLinuxMethodChannel.setMethodCallHandler(shutdownDirect); print("initState: creating cwtchnotifier, ffi"); if (Platform.isAndroid) { - var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList); + var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList, this); cwtch = CwtchGomobile(cwtchNotifier); } else if (Platform.isLinux) { - var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList); + var cwtchNotifier = + new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList, this); cwtch = CwtchFfi(cwtchNotifier); } else { - var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList); + var cwtchNotifier = + new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList, this); cwtch = CwtchFfi(cwtchNotifier); } print("initState: invoking cwtch.Start()"); diff --git a/lib/models/contact.dart b/lib/models/contact.dart index a6fa8234..4d6c8f30 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -1,6 +1,10 @@ +import 'dart:ffi'; + import 'package:cwtch/main.dart'; import 'package:cwtch/models/message_draft.dart'; import 'package:cwtch/models/profile.dart'; +import 'package:cwtch/themes/opaque.dart'; +import 'package:cwtch/views/contactsview.dart'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -101,7 +105,7 @@ class ContactInfoState extends ChangeNotifier { keys = Map>(); } - String get nickname => this._nickname + (this._messageDraft.isEmpty() ? "" : "*"); + String get nickname => this._nickname; String get savePeerHistory => this._savePeerHistory; String? get acnCircuit => this._acnCircuit; @@ -354,4 +358,61 @@ class ContactInfoState extends ChangeNotifier { this.messageCache.updateTranslationEvent(messageID, translation); notifyListeners(); } + + // Contact Attributes. Can be set in Profile Edit View... + List attributes = [null, null, null]; + void setAttribute(int i, String? value) { + this.attributes[i] = value; + notifyListeners(); + } + + ProfileStatusMenu availabilityStatus = ProfileStatusMenu.available; + void setAvailabilityStatus(String status) { + switch (status) { + case "available": + availabilityStatus = ProfileStatusMenu.available; + break; + case "busy": + availabilityStatus = ProfileStatusMenu.busy; + break; + case "away": + availabilityStatus = ProfileStatusMenu.away; + break; + default: + ProfileStatusMenu.available; + } + notifyListeners(); + } + + Color getBorderColor(OpaqueThemeType theme) { + if (this.isBlocked) { + return theme.portraitBlockedBorderColor; + } + if (this.isOnline()) { + switch (this.availabilityStatus) { + case ProfileStatusMenu.available: + return theme.portraitOnlineBorderColor; + case ProfileStatusMenu.away: + return theme.portraitOnlineAwayColor; + case ProfileStatusMenu.busy: + return theme.portraitOnlineBusyColor; + } + } + return theme.portraitOfflineBorderColor; + } + + String augmentedNickname(BuildContext context) { + return this.nickname + (this.availabilityStatus == ProfileStatusMenu.available ? "" : " (" +this.statusString(context) + ")"); + } + + String statusString(BuildContext context) { + switch (this.availabilityStatus) { + case ProfileStatusMenu.available: + return AppLocalizations.of(context)!.availabilityStatusAvailable; + case ProfileStatusMenu.away: + return AppLocalizations.of(context)!.availabilityStatusAway; + case ProfileStatusMenu.busy: + return AppLocalizations.of(context)!.availabilityStatusBusy; + } + } } diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index c60f222e..c425ae0b 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -33,11 +33,11 @@ class FileMessage extends Message { int fileSize = shareObj['s'] as int; String fileKey = rootHash + "." + nonce; - if (metadata.attributes["file-downloaded"] == "true") { - if (!Provider.of(context).downloadKnown(fileKey)) { - Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, fileKey); - } - } + // if (metadata.attributes["file-downloaded"] == "true") { + // if (!Provider.of(context).downloadKnown(fileKey)) { + Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, fileKey); + // } + //} if (!validHash(rootHash, nonce)) { return MessageRow(MalformedBubble(), index); diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 0abe9b5b..0c43d519 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -1,9 +1,12 @@ import 'dart:convert'; +import 'package:cwtch/config.dart'; import 'package:cwtch/models/remoteserver.dart'; import 'package:flutter/widgets.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import '../themes/opaque.dart'; +import '../views/contactsview.dart'; import 'contact.dart'; import 'contactlist.dart'; import 'filedownloadprogress.dart'; @@ -398,4 +401,40 @@ class ProfileInfoState extends ChangeNotifier { this._downloads.remove(fileKey); notifyListeners(); } + + // Profile Attributes. Can be set in Profile Edit View... + List attributes = [null, null, null]; + void setAttribute(int i, String? value) { + this.attributes[i] = value; + notifyListeners(); + } + + ProfileStatusMenu availabilityStatus = ProfileStatusMenu.available; + void setAvailabilityStatus(String status) { + switch (status) { + case "available": + availabilityStatus = ProfileStatusMenu.available; + break; + case "busy": + availabilityStatus = ProfileStatusMenu.busy; + break; + case "away": + availabilityStatus = ProfileStatusMenu.away; + break; + default: + ProfileStatusMenu.available; + } + notifyListeners(); + } + + Color getBorderColor(OpaqueThemeType theme) { + switch (this.availabilityStatus) { + case ProfileStatusMenu.available: + return theme.portraitOnlineBorderColor; + case ProfileStatusMenu.away: + return theme.portraitOnlineAwayColor; + case ProfileStatusMenu.busy: + return theme.portraitOnlineBusyColor; + } + } } diff --git a/lib/themes/opaque.dart b/lib/themes/opaque.dart index cd3f6934..1bd01d03 100644 --- a/lib/themes/opaque.dart +++ b/lib/themes/opaque.dart @@ -106,6 +106,9 @@ abstract class OpaqueThemeType { get portraitProfileBadgeColor => red; get portraitProfileBadgeTextColor => red; + get portraitOnlineAwayColor => Color(0xFFFFF59D); + get portraitOnlineBusyColor => Color(0xFFEF9A9A); + // dropshaddpow // todo: probably should not be reply icon color in messagerow get dropShadowColor => red; diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index f1041d11..7e9a700b 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -38,6 +38,11 @@ class _AddEditProfileViewState extends State { final ctrlrPass = TextEditingController(text: ""); final ctrlrPass2 = TextEditingController(text: ""); final ctrlrOnion = TextEditingController(text: ""); + + final ctrlrAttribute1 = TextEditingController(text: ""); + final ctrlrAttribute2 = TextEditingController(text: ""); + final ctrlrAttribute3 = TextEditingController(text: ""); + ScrollController controller = ScrollController(); late bool usePassword; late bool deleted; @@ -93,42 +98,89 @@ class _AddEditProfileViewState extends State { child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Visibility( visible: Provider.of(context).onion.isNotEmpty, - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - MouseRegion( - cursor: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic, - child: GestureDetector( - // don't allow setting of profile images if the image previews experiment is disabled. - onTap: Provider.of(context, listen: false).disableFilePicker || - !Provider.of(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment) - ? null - : () { - filesharing.showFilePicker(context, MaxImageFileSharingSize, (File file) { - var profile = Provider.of(context, listen: false).onion; - // Share this image publicly (conversation handle == -1) - Provider.of(context, listen: false).cwtch.ShareFile(profile, -1, file.path); - // update the image cache locally - Provider.of(context, listen: false).imagePath = file.path; - }, () { - final snackBar = SnackBar( - content: Text(AppLocalizations.of(context)!.msgFileTooBig), - duration: Duration(seconds: 4), - ); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - }, () {}); - }, - child: ProfileImage( - imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) - ? Provider.of(context).imagePath - : Provider.of(context).defaultImagePath, - diameter: 120, - tooltip: - Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "", - maskOut: false, - border: theme.theme.portraitOnlineBorderColor, - badgeTextColor: theme.theme.portraitContactBadgeTextColor, - badgeColor: theme.theme.portraitContactBadgeColor, - badgeEdit: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment)))) - ])), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + MouseRegion( + cursor: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic, + child: GestureDetector( + // don't allow setting of profile images if the image previews experiment is disabled. + onTap: Provider.of(context, listen: false).disableFilePicker || + !Provider.of(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment) + ? null + : () { + filesharing.showFilePicker(context, MaxImageFileSharingSize, (File file) { + var profile = Provider.of(context, listen: false).onion; + // Share this image publicly (conversation handle == -1) + Provider.of(context, listen: false).cwtch.ShareFile(profile, -1, file.path); + // update the image cache locally + Provider.of(context, listen: false).imagePath = file.path; + }, () { + final snackBar = SnackBar( + content: Text(AppLocalizations.of(context)!.msgFileTooBig), + duration: Duration(seconds: 4), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, () {}); + }, + child: ProfileImage( + imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) + ? Provider.of(context).imagePath + : Provider.of(context).defaultImagePath, + diameter: 120, + tooltip: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) + ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage + : "", + maskOut: false, + border: theme.theme.portraitOnlineBorderColor, + badgeTextColor: theme.theme.portraitContactBadgeTextColor, + badgeColor: theme.theme.portraitContactBadgeColor, + badgeEdit: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment)))) + ]), + SizedBox( + width: MediaQuery.of(context).size.width / 2, + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(5.0), + child: CwtchTextField( + controller: ctrlrAttribute1, + multiLine: false, + onChanged: (profileAttribute1) { + String onion = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-attribute-1", profileAttribute1); + Provider.of(context, listen: false).attributes[0] = profileAttribute1; + }, + hintText: Provider.of(context).attributes[0] ?? AppLocalizations.of(context)!.profileInfoHint!)), + Padding( + padding: EdgeInsets.all(5.0), + child: CwtchTextField( + controller: ctrlrAttribute2, + multiLine: false, + onChanged: (profileAttribute2) { + String onion = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-attribute-2", profileAttribute2); + Provider.of(context, listen: false).attributes[1] = profileAttribute2; + }, + hintText: Provider.of(context).attributes[1] ?? AppLocalizations.of(context)!.profileInfoHint2!)), + Padding( + padding: EdgeInsets.all(5.0), + child: CwtchTextField( + controller: ctrlrAttribute3, + multiLine: false, + onChanged: (profileAttribute3) { + String onion = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-attribute-3", profileAttribute3); + Provider.of(context, listen: false).attributes[2] = profileAttribute3; + }, + hintText: Provider.of(context).attributes[2] ?? AppLocalizations.of(context)!.profileInfoHint3!)), + ], + )) + ], + )), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 20, diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 8a4fc1ec..8a37a76e 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -25,6 +25,8 @@ import 'messageview.dart'; enum ShareMenu { copyCode, qrcode } +enum ProfileStatusMenu { available, away, busy } + class ContactsView extends StatefulWidget { const ContactsView({Key? key}) : super(key: key); @@ -137,12 +139,43 @@ class _ContactsViewState extends State { ? Provider.of(context).imagePath : Provider.of(context).defaultImagePath, diameter: 42, - border: Provider.of(context).isOnline - ? Provider.of(context).current().portraitOnlineBorderColor - : Provider.of(context).current().portraitOfflineBorderColor, + border: Provider.of(context).getBorderColor(Provider.of(context).theme), badgeTextColor: Colors.red, badgeColor: Colors.red, ), + PopupMenuButton( + icon: Icon(Icons.online_prediction), + tooltip: AppLocalizations.of(context)!.availabilityStatusTooltip, + splashRadius: Material.defaultSplashRadius / 2, + onSelected: (ProfileStatusMenu item) { + String onion = Provider.of(context, listen: false).onion; + switch (item) { + case ProfileStatusMenu.available: + Provider.of(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-status", "available"); + break; + case ProfileStatusMenu.away: + Provider.of(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-status", "away"); + break; + case ProfileStatusMenu.busy: + Provider.of(context, listen: false).cwtch.SetProfileAttribute(onion, "profile.profile-status", "busy"); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: ProfileStatusMenu.available, + child: Text(AppLocalizations.of(context)!.availabilityStatusAvailable!,), + ), + PopupMenuItem( + value: ProfileStatusMenu.away, + child: Text(AppLocalizations.of(context)!.availabilityStatusAway!,), + ), + PopupMenuItem( + value: ProfileStatusMenu.busy, + child: Text(AppLocalizations.of(context)!.availabilityStatusBusy!,), + ), + ], + ), SizedBox( width: 10, ), diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 99cb011f..a66a4561 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -196,7 +196,7 @@ class _MessageViewState extends State { ? Provider.of(context).imagePath : Provider.of(context).defaultImagePath, diameter: 42, - border: Provider.of(context).current().portraitOnlineBorderColor, + border: Provider.of(context).getBorderColor(Provider.of(context).theme), badgeTextColor: Colors.red, badgeColor: Provider.of(context).theme.portraitContactBadgeColor, badgeIcon: Provider.of(context).isGroup @@ -230,14 +230,15 @@ class _MessageViewState extends State { ), Expanded( child: Container( - height: 24, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration(), - child: Text( - Provider.of(context).nickname, - overflow: TextOverflow.clip, - maxLines: 1, - ))) + height: 42, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(), + child: Align( + alignment: Alignment.centerLeft, child: Text( + Provider.of(context).augmentedNickname(context), + overflow: TextOverflow.clip, + maxLines: 1, + )))) ]), actions: appBarButtons, ), diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 35ec44e2..50a1b619 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -96,8 +96,8 @@ class _PeerSettingsViewState extends State { child: Container( margin: EdgeInsets.all(10), padding: EdgeInsets.all(2), - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ + Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ ProfileImage( imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? Provider.of(context).imagePath @@ -107,7 +107,23 @@ class _PeerSettingsViewState extends State { border: settings.theme.portraitOnlineBorderColor, badgeTextColor: settings.theme.portraitContactBadgeTextColor, badgeColor: settings.theme.portraitContactBadgeColor, - badgeEdit: false) + badgeEdit: false), + SizedBox( + width: MediaQuery.of(context).size.width / 2, + child: Column(children: [ + Padding( + padding: EdgeInsets.all(1), + child: SelectableText(Provider.of(context, listen: false).attributes[0] ?? ""), + ), + Padding( + padding: EdgeInsets.all(1), + child: SelectableText(Provider.of(context, listen: false).attributes[1] ?? ""), + ), + Padding( + padding: EdgeInsets.all(1), + child: SelectableText(Provider.of(context, listen: false).attributes[2] ?? ""), + ) + ])) ]), Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index 8411214b..01eba222 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -44,20 +44,16 @@ class _ContactRowState extends State { color: Provider.of(context).selectedConversation == contact.identifier ? Provider.of(context).theme.backgroundHilightElementColor : Colors.transparent, child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( - padding: const EdgeInsets.all(6.0), //border size - child: ProfileImage( + padding: const EdgeInsets.all(6.0), //border size + child: ProfileImage( badgeCount: contact.unreadMessages, badgeColor: Provider.of(context).theme.portraitContactBadgeColor, badgeTextColor: Provider.of(context).theme.portraitContactBadgeTextColor, diameter: 64.0, imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? contact.imagePath : contact.defaultImagePath, disabled: !contact.isOnline(), - border: contact.isOnline() - ? Provider.of(context).theme.portraitOnlineBorderColor - : contact.isBlocked - ? Provider.of(context).theme.portraitBlockedBorderColor - : Provider.of(context).theme.portraitOfflineBorderColor), - ), + border: contact.getBorderColor(Provider.of(context).theme), + )), Expanded( child: Padding( padding: EdgeInsets.all(10.0), @@ -69,7 +65,7 @@ class _ContactRowState extends State { clipBehavior: Clip.hardEdge, decoration: BoxDecoration(), child: Text( - contact.nickname, //(contact.isInvitation ? "invite " : "non-invite ") + (contact.isBlocked ? "blokt" : "nonblokt"),// + contact.augmentedNickname(context) + (contact.messageDraft.isEmpty() ? "" : "*"), style: TextStyle( fontSize: Provider.of(context).theme.contactOnionTextSize(), diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 45884a78..8920bdee 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -505,7 +505,7 @@ void modalShowTranslation(BuildContext context, ProfileInfoState profile, Settin var bubble = StaticMessageBubble( profile, settings, - MessageMetadata(profile.onion, Provider.of(context).identifier, 1, DateTime.now(), "blodeuwedd", null, null, null, true, false, false, ""), + MessageMetadata(profile.onion, Provider.of(context, listen: false).identifier, 1, DateTime.now(), "blodeuwedd", null, null, null, true, false, false, ""), Row(children: [ Provider.of(context).translation == "" ? Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart index 9c77280f..021f4bf8 100644 --- a/lib/widgets/textfield.dart +++ b/lib/widgets/textfield.dart @@ -68,6 +68,7 @@ class _CwtchTextFieldState extends State { decoration: InputDecoration( errorMaxLines: 2, hintText: widget.hintText, + hintStyle: TextStyle(color: (theme.current().mainTextColor as Color).withOpacity(0.5)), floatingLabelBehavior: FloatingLabelBehavior.never, filled: true, focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 1.0)), From db421f2691098f6d799ad6dcf2c8412dad922d97 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 4 Apr 2023 19:57:35 -0700 Subject: [PATCH 2/4] Initial Tails Support / Center align profile attributes / biotext --- lib/views/peersettingsview.dart | 6 ++-- linux/cwtch-tails.yml | 55 +++++++++++++++++++++++++++++++++ linux/cwtch.tails.sh | 3 ++ linux/install-tails.sh | 27 ++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 linux/cwtch-tails.yml create mode 100755 linux/cwtch.tails.sh create mode 100755 linux/install-tails.sh diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 50a1b619..a066a315 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -113,15 +113,15 @@ class _PeerSettingsViewState extends State { child: Column(children: [ Padding( padding: EdgeInsets.all(1), - child: SelectableText(Provider.of(context, listen: false).attributes[0] ?? ""), + child: SelectableText(Provider.of(context, listen: false).attributes[0] ?? "", textAlign: TextAlign.center,), ), Padding( padding: EdgeInsets.all(1), - child: SelectableText(Provider.of(context, listen: false).attributes[1] ?? ""), + child: SelectableText(Provider.of(context, listen: false).attributes[1] ?? "", textAlign: TextAlign.center,), ), Padding( padding: EdgeInsets.all(1), - child: SelectableText(Provider.of(context, listen: false).attributes[2] ?? ""), + child: SelectableText(Provider.of(context, listen: false).attributes[2] ?? "", textAlign: TextAlign.center,), ) ])) ]), diff --git a/linux/cwtch-tails.yml b/linux/cwtch-tails.yml new file mode 100644 index 00000000..36260efe --- /dev/null +++ b/linux/cwtch-tails.yml @@ -0,0 +1,55 @@ +--- +# TODO: This can likely be restricted even further, especially in regards to the ADD_ONION pattern +- apparmor-profiles: + - '/home/amnesia/.local/lib/cwtch/cwtch' + users: + - 'amnesia' + commands: + AUTHCHALLENGE: + - 'SAFECOOKIE .*' + SETEVENTS: + - 'CIRC WARN ERR' + - 'CIRC ORCONN INFO NOTICE WARN ERR HS_DESC HS_DESC_CONTENT' + GETINFO: + - 'net/listeners/socks' + GETCONF: + - 'DisableNetwork' + SETCONF: + - 'DisableNetwork.*' + ADD_ONION: + - '.*' + DEL_ONION: + - '.+' + HSFETCH: + - '.+' + events: + CIRC: + suppress: true + ORCONN: + suppress: true + INFO: + suppress: true + NOTICE: + suppress: true + WARN: + suppress: true + ERR: + suppress: true + HS_DESC: + response: + - pattern: '650 HS_DESC CREATED (\S+) (\S+) (\S+) \S+ (.+)' + replacement: '650 HS_DESC CREATED {} {} {} redacted {}' + - pattern: '650 HS_DESC UPLOAD (\S+) (\S+) .*' + replacement: '650 HS_DESC UPLOAD {} {} redacted redacted' + - pattern: '650 HS_DESC UPLOADED (\S+) (\S+) .+' + replacement: '650 HS_DESC UPLOADED {} {} redacted' + - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH' + replacement: '650 HS_DESC REQUESTED {} NO_AUTH' + - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH \S+ \S+' + replacement: '650 HS_DESC REQUESTED {} NO_AUTH redacted redacted' + - pattern: '650 HS_DESC RECEIVED (\S+) NO_AUTH \S+ \S+' + replacement: '650 HS_DESC RECEIVED {} NO_AUTH redacted redacted' + - pattern: '.*' + replacement: '' + HS_DESC_CONTENT: + suppress: true \ No newline at end of file diff --git a/linux/cwtch.tails.sh b/linux/cwtch.tails.sh new file mode 100755 index 00000000..ee3f419b --- /dev/null +++ b/linux/cwtch.tails.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# Start Cwtch with Tails +exec env CWTCH_TAILS=true LD_LIBRARY_PATH=~/.local/lib/cwtch/:~/.local/lib/cwtch/Tor ~/.local/lib/cwtch/cwtch \ No newline at end of file diff --git a/linux/install-tails.sh b/linux/install-tails.sh new file mode 100755 index 00000000..3835cd6b --- /dev/null +++ b/linux/install-tails.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +mkdir -p ~/.local/bin +sed "s|~|$HOME|g" cwtch.home.sh > ~/.local/bin/cwtch +chmod a+x ~/.local/bin/cwtch + +mkdir -p ~/.local/share/icons +cp cwtch.png ~/.local/share/icons + +mkdir -p ~/.local/share/cwtch +cp -r data ~/.local/share/cwtch + +mkdir -p ~/.local/lib/cwtch +cp -r lib/* ~/.local/lib/cwtch + +mkdir -p ~/.local/share/applications +sed "s|~|$HOME|g" cwtch.home.desktop > $HOME/.local/share/applications/cwtch.desktop +chmod a+x $HOME/.local/share/applications/cwtch.desktop + +# Tails needs to be have been setup up with an Administration account +# https://tails.boum.org/doc/first_steps/welcome_screen/administration_password/ +# Make Auth Cookie Readable +sudo chmod o+r /var/run/tor/control.authcookie +# Copy Onion Grater Config +sudo cp cwtch-tails.yml /etc/onion-grater.d/cwtch.yml +# Restart Onion Grater so the Config Takes effect +sudo systemctl restart onion-grater.service \ No newline at end of file From e1c0960fee2a51af3faca4a18f54349c5d64778b Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 4 Apr 2023 20:33:59 -0700 Subject: [PATCH 3/4] Update Cwtch Version --- LIBCWTCH-GO.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 53bd43c9..d0989af0 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2023-03-16-15-07-v0.0.3-1-g50c853a \ No newline at end of file +2023-04-04-19-59-v0.0.3-12-g0e05650 \ No newline at end of file From 80fed2e57b524713fa6ba7bb838836e238df53a3 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 4 Apr 2023 21:56:50 -0700 Subject: [PATCH 4/4] Update Goldens --- test/textfield_form_final.png | Bin 6016 -> 6015 bytes test/textfield_form_init.png | Bin 5171 -> 5171 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/textfield_form_final.png b/test/textfield_form_final.png index 64d499ab8ac87dba176d4f4d135c803185311ce8..27e9d9ecda8451d7ad75bbb209dc89e4909bf5eb 100644 GIT binary patch literal 6015 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 PAjtNFqg6TD_n-d-jBZj& diff --git a/test/textfield_form_init.png b/test/textfield_form_init.png index d3f7d9806451b31a868f8b2f0895a958db91f44a..1fad878171df9f0cf3b2bdc4aaaaabf1042768da 100644 GIT binary patch literal 5171 zcmeHLSy)rqvfebdMo}<88*v~c1Vurl8AKTj#wbJ(Y`|cnGPFQsmPzKJH42Ith#(+C zKtV)61jL94v9;BJ1e^kfL5@uzgi#0~%sFd^d%o|!+^73`@{+x4)%vUIUsbi%+Sg9l zS#4OiWgP@T8%Wkf2MChafFLe4*#6wPzd1Jk&Z=8{M^iB4inR$D5+~Ka?;~|HqSln)x!|T?)Uag#bDetbYI(F@b z>xH{FqJ}P?Ev*?@eMCK=(9-=$uqwtx6S3`=W9z1iuBUf|D}2$&PzX8XvNlsoZHIP! z1<@*?hd7u<*=WNZ zZl>w!#q-}(R#wqsbk}D=a*#F>`Knk&1A??P3E7@Mt%3@vM8vIQi4c^Lf$&gXyAeuM z);UZypBZ5-Gk23>Mp|-i6nAu_KdV9_|~ylxp&nxH4?$zD1Ki zDJW==G`&-A*h3mlNjZshP|Uk_`CGI0(mcWpzn9=pRZB6Z&7D`n|M6HH*lsh$ z!!a73Zh&5{%AH2k{~jBboL2oTo1cSYg#Dd%X+B5Vw8k6-K|eq12^4Vh z^UWVWPMfD5VS7xb<3v_B`Bv(2Mgq4uV{WLRhFm$=+Gcl1Gw=5lUc+ahBdIB$FOIi2 zV!QS1r37{KKd_A6NO+J#a7RO_vz6;w0twNND9H2#U9!Gy4YANdtEX}{z zHP&l%$2qjVIz7MR;-hhYFJ^DcTW$)^zk*U)7x&`>g}zuG|A;rSp6y)gj3>>!>L2GS z4a)mTyuCb(L+j?Yxz(F7_x7a^_%X^ehg}nsm7v6HA1XM4#;%a@p89N;u5;UMswUG% zIn>1uWl5qozpB!?w-F9OfrGCW$27Ni90-qRIO}jnRMzMyl;>3K@nkmUW?td;G{rg( z`YQMsN$&FR(00z9m_N1klYOH3t+>hHu;4xD7h}u1oV`0qQBLXYFtd*woOV-(s@w;E?>Uv@Q;=+Sir4P^Uba{hIzWAf02m;14v znGH&n<2 z*Pr-T{C2kYmZM)Z4fa3cs{A7l72>WZr~US~RT4ri|5ypVWG>TxpI(Ve*?n>)rMCLp z+(Z&Rwk%f^wpGL4Gib>Q_|O^D_}ovd&O9pzWwdH*dS?8`_8B)7m|AhAMACjKtZq|r zH#FxovniYZHNF3L*HfI9SHt4pH$qU0mnzbM2-nNUGP0i!vm30*r=H&CjyAE$N9oDu zkytWQZc2d{(-t&t9^EdM#71`g0$E0FFeF^RK~qE;n2N+#U@slYyyHfe+B#EltkAF3 zy?WoCF>8MX718x+3`FL>+MiR&AJCL|m5wk{@#nWqeyomtH)%dMniVS5R1y&V3?%^t zYVY^A+y_A6!lzC;eI|_R4o-FNvR2#!84{A|DhIXRIE3}glSO$hDqbrk)MkMd{UEnL zdRgaAIj8iWGG_=fEMXvWsIx;GF|VD=oD7|FC+40N*!&Uo{d_x04sxFtD`Re{qPJr` zXXkw`7sz{7H-*jGfrjT`f;6}WLtphPYWgbr)nZNZ8vz8>j**cD%`$l@14Wm_Q$gR% zjxxEP=95gjIbe2EP$ppANn$MDK&V+y1bkRnZ;b|a3S$+IvfUbI#*CQEcjgbrPd?O<&^axFcM9;) zeU!)^-3X`UDx|?yctqteHG=_<+)YjkfkAAyAst*n*H65LBeakbAxOWPXg!AjiLv7j zViyXA|62t!fC2E?>#)V!U=YW{R9iee;)KL}J`T?b2Q6P2Mk_HOvX!Ju_Qfuh1Fsm% zldVMSDmjPky(b{3(ByQePShMbjz=<1m@a3fOGOOUzHyA$GdSG1bXiRK-+3TO+i_Fo z<|r+#DsH)tUniYUg?Y57c*GqXoZ{EIZNiv*-=qutY+LTGc3rqEHOOS1KrrxG&Xhc`S)8yX|Bb@6wdfi?Y;M0S{r>g`B_Mj3x3s_jmgzc0#|&sMMs7pTCl z+FBqDEMS!q4Z{7)fPg|HBAX9S&bW@q@rQ%>1cI;ub50z?dK!S;89HV|wN(dbmSw%# z1Q*WWvm|aE>?mU~sjuy|eZqZJB-Roz(s&s;^%lKM>KdDYReT!@(9$gFT~$L244d;9 zPbf$jrikzflYPgzW2Z##t&Rno-G&#z`;>}7yi5s8cnjOs4<)i^;T-X%Bh-}ONLrOK zZ(uiDjgStyGJy)_4II%%q=PDWhdM!jffXLp5Ndm1!5Scd6)wnQJ&oXyd#DJtgYXDb zgK!nL@)83!3)srgC4`zHJaSeUa~THtAdfWI3=1rjF-tN*($*u^U1PyY<$muYwzWI9 zC)E9rt^J46%gB(yX|y+!yG8Q&Ik}9iz*q~L)lXbPPAxj(bA#fASv@zy6+nE@YR}`= z64;mZeJPxCH8luAU$!Tb~`rN5RwnV1;2bLe? z^t`94mlrwx=g#@2&48Dhl6;PNc}yS=-W+z6>Sqi ze`t0rC3p5ko*N;rpc8Cks9#X1@}wilutRG@^a+7xQ3mMQh6GB_>tUC;;Ogkag_*x5Bvva^jgKRzW^}6f5%&4>9h_yB3S0!xj<2BHx~pE$_cBZ~xxV5UW>-*| zi^VROgIC8xsA2zTHW$g!PsxMzcl1!!6s9VgXP|@_G9_(|MPq_9T{?O)*9An&A?*$-jnIF z#W-;9M%w8&R23pgzo3Q%Y%CpIZdx#vfnvA~rB$`s#H&^-MPXkfi z`Keau$h(r?zAaE?JHD=PY zXgyi^-yGewqglSEVQ-DLQmE6hZ+d4g-1h}alWNZ3weF~BSolL9iP34ZI5$v_*|yBiRMT3gaF1n2T7f7A;j6@Dryz!Jd#?v7aO3Vi@x)~=A@waq zF=Zp1fa~AXj*KKkos#tu_w)aeaw}JG4oB3=+QUoh9_939e8tOVArYk zuzlZp%^AHHN5~5x-?i$x0|$CJB5<=A^j=W^@_s$LdUVfGHyirB6hHXZsjj#N0R5{n zg#s>>^Yb(@4&9Q5DiU<9#q;JK2ZzT=GZes~MZT>nAp-0+F{vNKgh>}#XN*O20O?@C zsD*_2v8ScwmGACfCXB%8ucj-2IW;q%TR**|(4L@W(G#CJt*$|uO2wh=rcTiPO}5sD zi3z^kXz-O|e*bm=-Q2z!)*DkQK%Q=Fff2u2V zD@w?fUPXG=pi^`i-8t;&N=gvt z-Gn6OQheZFT@zv)E2lV}Z*@yoG4dclnX1@1=RjwWvVzQ6Ifp2giqY6+P2OpAh;H-c z!w2!ewdlQ+iDvR%uZazUxb{fUIR4L{(6akrL@cNum%sfuR#x?mjs8x!?2L=l;7tzx+wg+Izoi?X}*u&RU7OY=u8` z;M)Tb1RWxn<7^;EP!56w)Dc2JVyMw?0KNoBHh3&l(jhek9`=&31X~37gd$uYL68`e zfIDw{KZDD3jCNYP!CqiHtu|htv-wr?)uc@YTH%1rg#&vUgpNM?c^GXWi%T}-u(Vc509WzGr1RnJo~OT3!D+x~Xi5#7`#ZZY^777(QJFu$*lJj)ajssG;k zM5A%i`Py!e?GN;t?N#L_gt7d`@oA3ch)R2A3*UA93}2@2;R$fD>V za`7+UopI9w1#Yp9``igsw>5lXSQpj;1wb zg{`tSWkO~v6biI7?Jk;QY$HPJni7oSIlm}0m28VZ@2GDI?ON*{uD4@v{bec`In$yQ zcG7f1$8;%-?OV5EN9K>pGAip9VnWM0oBOqhG7Xb%e=ZR;oj2F~RT<{Jx76_)U^oJg54-3emw$zIY#sa^4STZh$o`3(Cf1J&JAHwkr#oJ6v< zZZmP1(#9%kx<@MK6{#1EO^&>_RV!ET(708imo@e<+x|DLT$_TNcnejr)yTt;_LHOl zy`|5IWvQ)mZzAIrc**q({;ahxP1P*^qNDFbp>>#&yOn8jzo#0e`Ys=VziPz|Fy#K; z?K7X(S(DZp6EpjJ1?ircQQw!TC);}_#p)@KHjbi~JfWjhhA1xuSX4h7CxXRp~`YY+8CRdbo(zT>N7)u*pI znrByuEB1$Kw}f;x4bsSaI8(Dv; zs|x>+Im{}{Rt@aGUbyCZn{0m{T;-{sS+g7rXqF8CZ}^=2j?`LJQ~T5-H?8->P-*v| zRd!YH0QUjG*<92U8!Sw_IpBDwE@k#ZQ+40mrTCu0mErPWbosQckCnK#&?b2Xh2b#pD(p&-w> zc%mbF%_(c20OVqDTmsb)y^E7*p@<)k8=vkNo2>{eGpbLEDHJ#G#GXjdEnTalU069P zN30E49IoA%oAET6b1+7kfK>bS5klf(sQ)8cXMwG%eHyXFZPeyvqK8Lsu%6^hCZDX* zGEt4Ot`u0rt@cq|OY|t)eTCI_^L|6ep>r+Kr?F-dveIZ*f7Qf~D`!woNZ;ABgWdV+ z=2tz>zdb~YyXE^6AyyjmeC%N_h@>-Y@e3z2Bah<-mn2MqLoHc*FagQa;NS_)Mo_flh8m(hufNTCyok4f? z^SRBxDteFP#GqUR+H8$h4nQt>%VMa6Bd{mdfmz_Fv1~typsppw<)Hy}>YdT9@gvz6 zs0WFr-k(U}>V3(F*}_n`&9o9$=@bloN25nP#3XM5dc7_Mxl}M!u}TZe>ILf7XH)*= zm%R>}Tl4iPsfejF=U!$@g4ge|o2QSWmQnaFG|fxMmJ)?z6{;R4*8{3yDf7?1u{C zd{i1%X0ynC!xvm0hjH>vFmou-9!wia&-|h0zBEDHDpqKbuV0?(nV4uYDxFhj`x(iS z$3=M4SF#A8WP3et8A#l0GA{Zv=%5zx!szD^tQ9{pKYtoon~6vr!!I8IuFeB$eRlmP zkIhSlJ*yf;n}T1gDGq4Y4LDADX^2$K-7U)4yhzyQW4LC2m}So*5UHW?iVd*&QaA)% zHbA@Hg(GM|pjs-yG`Jl_Ymo+F%?k&lfQu>sC%0xPJVwxZf%P*q7LHMFpf=Wd zyNsV0P;ESS<6f}?eL|VMAZ~F=3=Iya2ye=B)U3%0{H3e>ZChsSnflP2W0f~yO>Plq zP@~H5rjuyb8W>9lPmSK?h*A`70XDNu9cg0(6Tngo^#SH&8z7Ao5I~@~Ai?vU7=(l| z{JaTD8@%C8I6^`i7U;`i%`SvP(7Q{IXkXwk-4Q{%zJaw!VW|gUE#aDA1=jLO5qbJ7 ze8%_)>H&=UNgml{3Jb#Cd{50`!t8*r4yU8I3<|gn+VI!Lc4X|7%~m*!%GmX*_IiBL z*zS!7s)z_-RA0ePCk03ojC$dJbzu9YHZ+1=ZHaD7v;5oy4%`B-~*%ry0<%W`RCV%7D;(}1PeZ}u(sr%^}?M?wp zVp&`FWq7&P`6i~OAQIgF-33tU7Cy>y*<+vTe!CY`%m~n9g)|8o9~Ofk^Z5UHgV*dN zQxchOYRaa6@7Q`WchqI+JbC=0V7^R8x>kMscp=-Wwkoy0YI%I;x=G+)7}&^Tzj+}` zdsr>0nVB6F5nz6b?ybi(W`BoUu>*HEUBU^rNeB$TOaF>-V2RkSuay2Jf&TZ_Xz>?$ z{9LGy=Zv+_y=d4er&O;{dpa`HDg2R92k);cn^G+-S()gU?MK~41g)c`S zFhSEj(;J0!A5c|clPn+Ij-zdi{IK#0vFq~lhs~P3m959G^Oy0B#ziH);^KvP>x`6; zg*Ed0pEl*ei!M|+X;QQSWrz6Nl$?RTo~NbK0y{DWHkhJelS7$|{qi_Co_o_8hhDQV zB)6FXS^k2r^cI=&_2}j9V<_L6^&iM4afB7OBbj$3nNc>e->P^FSe@> zDcpveW;m#zfx(+Ao5V|fD;IS}4lv`m(kX1+W>+h(JW{r9#~!FV7iDQf4PDDkUK--h z5?!l%-|S?){yGJMY5u(aI zQ~_4&)}bQLv0He$dtpNMBU;0V{H~YEA|z1E_xN42^A5>l-^Rq@fDP~3Bo(m1QM(C$4s|-Mg=n~L4ouS zVgD9xlZT30(NmtG$@> zP=4)J{@Sr`9<%6!C***$+8ZTM#zenLil?wMq=v5T29#L^arx&J`grrePrp-oR&$vz#i8!zmx5KR%~%F`6+c5qjNHI z%7^%c7@^?F`E*I&9BwTz^|FEf6WV!Yln{BQ+0-keQU+LKG3i3pb!`8Mfw>c|8<@`9 zuLE8amn|GgYC+^F1hfBZ>F5)25g3q zyXWdguE48%Xpyfbz?Ix3F!x z=#wM6tL^_SefzIn9G6L-6T9W%n<&YzXlCPI)7d3w6%W@4$n2l0qw-#7W`?2xWBv)# z|AjW35GruNfey(3NBke3{QnSwDbyTjAPHt_4y)jL&+1(!0Oow#n`H4$qkL@^a#;dI7q|8-5bX_pM-) z6J1+3^HQF~b)zRpfK2bL0!}K5m zrso6<71xvpk@;7x*ITi07#;Syx;}?5+L$lKj#=r zETWg448PzFW%Wu*=CkgCEOC3sTbZAYi*uzT-#V)}&nyJf6~J`8KcRMFM=#CM-u~L9 z+>y)(P|NJVp9`#O6piG%z%M*{e&i56o+YS2sC&+-CCFIBbKT^VOPtM%9T+3SHnRDG zpneW)^ceOK2}!FxX4 zjHFh~v;%|ez?`@cT#j&1Un%ReiVax)3=9>PT{TnX0Y=l=rmQfXTN