Merge branch 'gherkin' of git.openprivacy.ca:cwtch.im/cwtch-ui into gherkin

This commit is contained in:
erinn 2022-02-02 14:40:49 -08:00
commit 4d0bb92495
32 changed files with 243 additions and 85 deletions

View File

@ -1,3 +1,4 @@
@env:persist
Feature: Basic Profile Management Feature: Basic Profile Management
Scenario: Error on Creating a Profile without a Display Name Scenario: Error on Creating a Profile without a Display Name
Given I wait until the widget with type 'ProfileMgrView' is present Given I wait until the widget with type 'ProfileMgrView' is present
@ -18,14 +19,18 @@ Feature: Basic Profile Management
Then I tap the "passwordCheckBox" widget Then I tap the "passwordCheckBox" widget
And I expect the text 'New Password' to be absent And I expect the text 'New Password' to be absent
And I take a screenshot And I take a screenshot
Then I fill the "displayNameFormElement" field with "Alice (<h1>hello</h1>)" Then I fill the "displayNameFormElement" field with "Alice (Unencrypted)"
Then I tap the "button" widget with label "Add new profile" Then I tap the "button" widget with label "Add new profile"
And I expect a "ProfileRow" widget with text "Alice (<h1>hello</h1>)" And I expect a "ProfileRow" widget with text "Alice (Unencrypted)"
And I take a screenshot And I take a screenshot
Then I tap the "ProfileRow" widget with label "Alice (<h1>hello</h1>)" Then I tap the "ProfileRow" widget with label "Alice (Unencrypted)"
And I expect the text 'Alice (<h1>hello</h1>) » Conversations' to be present And I expect the text "Alice (Unencrypted) » Conversations" to be present
And I take a screenshot And I take a screenshot
Scenario: Load Unencrypted Profile
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect a "ProfileRow" widget with text "Alice (Unencrypted)"
Scenario: Create Encrypted Profile Scenario: Create Encrypted Profile
Given I wait until the widget with type 'ProfileMgrView' is present Given I wait until the widget with type 'ProfileMgrView' is present
And I tap the button with tooltip "Add new profile" And I tap the button with tooltip "Add new profile"
@ -41,4 +46,45 @@ Feature: Basic Profile Management
And I take a screenshot And I take a screenshot
Then I tap the "ProfileRow" widget with label "Alice (Encrypted)" Then I tap the "ProfileRow" widget with label "Alice (Encrypted)"
And I expect the text 'Alice (Encrypted) » Conversations' to be present And I expect the text 'Alice (Encrypted) » Conversations' to be present
And I take a screenshot And I take a screenshot
Scenario: Load an Encrypted Profile by Unlocking it with a Password
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect the text 'Enter a password to view your profiles' to be absent
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
Then I expect the text 'Enter a password to view your profiles' to be present
When I fill the "unlockPasswordProfileElement" field with "password1"
And I tap the "button" widget with label "Unlock"
Then I expect a "ProfileRow" widget with text "Alice (Encrypted)"
Scenario: Load an Encrypted Profile by Unlocking it with a Password and Change the Name
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect the text 'Enter a password to view your profiles' to be absent
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
Then I expect the text 'Enter a password to view your profiles' to be present
When I fill the "unlockPasswordProfileElement" field with "password1"
And I tap the "button" widget with label "Unlock"
Then I expect a "ProfileRow" widget with text "Alice (Encrypted)"
When I tap the "IconButton" widget with tooltip "Edit Profile Alice (Encrypted)"
Then I expect the text 'Display Name' to be present
Then I fill the "displayNameFormElement" field with "Carol (Encrypted)"
And I tap the "button" widget with label "Save Profile"
And I wait until the widget with type 'ProfileMgrView' is present
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)"
Scenario: Delete an Encrypted Profile
Given I wait until the widget with type 'ProfileMgrView' is present
Then I expect the text 'Enter a password to view your profiles' to be absent
And I tap the button with tooltip "Unlock encrypted profiles by entering their password."
Then I expect the text 'Enter a password to view your profiles' to be present
When I fill the "unlockPasswordProfileElement" field with "password1"
And I tap the "button" widget with label "Unlock"
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)"
And I take a screenshot
When I tap the "IconButton" widget with tooltip "Edit Profile Carol (Encrypted)"
Then I expect the text 'Display Name' to be present
When I tap the button that contains the text "Delete"
Then I expect the text "Really Delete Profile" to be present
When I tap the "button" widget with label "Really Delete Profile"
And I wait until the widget with type 'ProfileMgrView' is present
Then I expect a "ProfileRow" widget with text "Carol (Encrypted)" to be absent

View File

@ -38,7 +38,9 @@ void main() {
// overrides // overrides
TapWidgetWithType(), TapWidgetWithType(),
TapWidgetWithLabel(), TapWidgetWithLabel(),
TapWidgetWithTooltip(),
ExpectWidgetWithText(), ExpectWidgetWithText(),
AbsentWidgetWithText(),
WaitUntilTypeExists(), WaitUntilTypeExists(),
ExpectTextToBePresent(), ExpectTextToBePresent(),
ExpectWidgetWithTextWithin(), ExpectWidgetWithTextWithin(),

View File

@ -37,6 +37,22 @@ StepDefinitionGeneric TapWidgetWithLabel() {
firstMatchOnly: true); firstMatchOnly: true);
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first); //Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
//print(wdg.debugDescribeChildren().first.) //print(wdg.debugDescribeChildren().first.)
await context.world.appDriver.scrollIntoView(finder);
await context.world.appDriver.tap(finder);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric TapWidgetWithTooltip() {
return given2<String, String, FlutterWorld>(
RegExp(r'I tap the {string} widget with tooltip {string}$'),
(ofType, text, context) async {
final finder = context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
context.world.appDriver.findBy(text, FindType.tooltip),
firstMatchOnly: true);
await context.world.appDriver.scrollIntoView(finder);
await context.world.appDriver.tap(finder); await context.world.appDriver.tap(finder);
await context.world.appDriver.waitForAppToSettle(); await context.world.appDriver.waitForAppToSettle();
}, },
@ -59,6 +75,23 @@ StepDefinitionGeneric ExpectWidgetWithText() {
); );
} }
StepDefinitionGeneric AbsentWidgetWithText() {
return given2<String, String, FlutterWorld>(
RegExp(r'I expect a {string} widget with text {string} to be absent$'),
(ofType, text, context) async {
final finder = context.world.appDriver.findByDescendant(
context.world.appDriver.findBy(widgetTypeByName(ofType), FindType.type),
context.world.appDriver.findBy(text, FindType.text),
firstMatchOnly: true);
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first);
//print(wdg.debugDescribeChildren().first.)
await context.world.appDriver.isAbsent(finder);
await context.world.appDriver.waitForAppToSettle();
},
);
}
StepDefinitionGeneric TapButtonWithText() { StepDefinitionGeneric TapButtonWithText() {
return given1<String, FlutterWorld>( return given1<String, FlutterWorld>(
RegExp(r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'), RegExp(r'I tap the {string} (?:button|element|label|icon|field|text|widget)$'),
@ -67,8 +100,7 @@ StepDefinitionGeneric TapButtonWithText() {
context.world.appDriver.findBy(Flwtch, FindType.type), context.world.appDriver.findBy(Flwtch, FindType.type),
context.world.appDriver.findBy(input1, FindType.key), context.world.appDriver.findBy(input1, FindType.key),
firstMatchOnly: true); firstMatchOnly: true);
//Text wdg = await context.world.appDriver.widget(finder, ExpectedWidgetResultType.first); await context.world.appDriver.scrollIntoView(finder);
//print(wdg.debugDescribeChildren().first.)
await context.world.appDriver.tap(finder); await context.world.appDriver.tap(finder);
await context.world.appDriver.waitForAppToSettle(); await context.world.appDriver.waitForAppToSettle();
}, },
@ -229,6 +261,8 @@ Type widgetTypeByName(String input1) {
return TorIcon; return TorIcon;
case "button": case "button":
return ElevatedButton; return ElevatedButton;
case "IconButton":
return IconButton;
case "ProfileRow": case "ProfileRow":
return ProfileRow; return ProfileRow;
default: default:

View File

@ -5,6 +5,7 @@ import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profilelist.dart'; import 'package:cwtch/models/profilelist.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/remoteserver.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
import 'package:cwtch/notification_manager.dart'; import 'package:cwtch/notification_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -197,7 +198,8 @@ class CwtchNotifier {
var senderHandle = data['RemotePeer']; var senderHandle = data['RemotePeer'];
var senderImage = data['Picture']; var senderImage = data['Picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!; var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages; var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
var currentTotal = contact!.totalMessages;
var isAuto = data['Auto'] == "true"; var isAuto = data['Auto'] == "true";
String? contenthash = data['ContentHash']; String? contenthash = data['ContentHash'];
var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier; var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier;
@ -218,6 +220,8 @@ class CwtchNotifier {
notificationManager.notify("New Message From Group!"); notificationManager.notify("New Message From Group!");
} }
RemoteServerInfoState? server = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(contact.server);
server?.updateSyncProgressFor(timestampSent);
} else { } else {
// This is dealt with by IndexedAcknowledgment // This is dealt with by IndexedAcknowledgment
EnvironmentConfig.debugLog("new message from group from yourself - this should not happen"); EnvironmentConfig.debugLog("new message from group from yourself - this should not happen");

View File

@ -1,6 +1,6 @@
{ {
"@@locale": "de", "@@locale": "de",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "en", "@@locale": "en",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"editProfile": "Edit Profile",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -239,7 +240,6 @@
"radioNoPassword": "Unencrypted (No password)", "radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password", "radioUsePassword": "Password",
"copiedToClipboardNotification": "Copied to Clipboard", "copiedToClipboardNotification": "Copied to Clipboard",
"editProfile": "Edit Profille",
"newProfile": "New Profile", "newProfile": "New Profile",
"defaultProfileName": "Alice", "defaultProfileName": "Alice",
"profileName": "Display name", "profileName": "Display name",

View File

@ -1,6 +1,6 @@
{ {
"@@locale": "es", "@@locale": "es",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",

View File

@ -1,22 +1,24 @@
{ {
"@@locale": "fr", "@@locale": "fr",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "editProfile": "Modifier le profil",
"torSettingsEnableCache": "Cache Tor Consensus", "settingTheme": "Utilisez des thèmes clairs",
"labelTorNetwork": "Tor Network", "torSettingsUseCustomTorServiceConfiguration": "Utiliser une configuration personnalisée du service Tor (torrc)",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "torSettingsUseCustomTorServiceConfigurastionDescription": "Remplacer la configuration par défaut de tor. Avertissement : Cela peut être dangereux. Ne l'activez que si vous savez ce que vous faites.",
"labelACNCircuitInfo": "ACN Circuit Info", "torSettingsErrorSettingPort": "Le numéro de port doit être compris entre 1 et 65535.",
"fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "torSettingsEnableCache": "Cache du Consensus Tor",
"fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsEnabledCacheDescription": "Mettre en cache le consensus Tor actuellement téléchargé pour le réutiliser la prochaine fois que Cwtch est ouvert. Cela permettra à Tor de démarrer plus rapidement. Lorsqu'il est désactivé, Cwtch purgera les données mises en cache au démarrage.",
"torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsEnabledAdvancedDescription": "Utilisez un service Tor existant sur votre système ou modifiez les paramètres du service Tor de Cwtch.",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsEnabledAdvanced": "Activer la configuration avancée de Tor",
"torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomSocksPortDescription": "Utiliser un port personnalisé pour les connexions de données au proxy Tor",
"torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomSocksPort": "Port SOCKS personnalisé",
"torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomControlPortDescription": "Utiliser un port personnalisé pour contrôler les connexions au proxy Tor",
"torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", "torSettingsCustomControlPort": "Port de contrôle personnalisé",
"torSettingsCustomSocksPort": "Custom SOCKS Port", "labelTorNetwork": "Réseau Tor",
"torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", "labelACNCircuitInfo": "Informations sur le circuit ACN",
"torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", "fileSharingSettingsDownloadFolderTooltip": "Parcourir pour sélectionner un autre dossier par défaut pour les fichiers téléchargés.",
"fileSharingSettingsDownloadFolderDescription": "Lorsque les fichiers sont téléchargés automatiquement (par exemple, les fichiers image, lorsque les aperçus d'image sont activés), un emplacement par défaut pour télécharger les fichiers est nécessaire.",
"descriptionACNCircuitInfo": "Informations détaillées sur le chemin que le réseau de communication anonyme utilise pour se connecter à cette conversation.",
"msgConfirmSend": "Êtes-vous sûr de vouloir envoyer", "msgConfirmSend": "Êtes-vous sûr de vouloir envoyer",
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe de", "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe de",
"msgFileTooBig": "La taille du fichier ne peut pas dépasser 10 Go", "msgFileTooBig": "La taille du fichier ne peut pas dépasser 10 Go",
@ -187,7 +189,6 @@
"deleteConfirmLabel": "Tapez SUPPRIMER pour confirmer", "deleteConfirmLabel": "Tapez SUPPRIMER pour confirmer",
"addNewProfileBtn": "Ajouter un nouveau profil", "addNewProfileBtn": "Ajouter un nouveau profil",
"enterProfilePassword": "Entrez un mot de passe pour consulter vos profils", "enterProfilePassword": "Entrez un mot de passe pour consulter vos profils",
"editProfile": "Modifier le profil",
"radioUsePassword": "Mot de passe", "radioUsePassword": "Mot de passe",
"radioNoPassword": "Non chiffré (pas de mot de passe)", "radioNoPassword": "Non chiffré (pas de mot de passe)",
"saveProfileBtn": "Sauvegarder le profil", "saveProfileBtn": "Sauvegarder le profil",
@ -205,7 +206,6 @@
"localePt": "Portugais", "localePt": "Portugais",
"localeDe": "Allemand", "localeDe": "Allemand",
"settingInterfaceZoom": "Niveau de zoom", "settingInterfaceZoom": "Niveau de zoom",
"settingTheme": "Thème",
"themeLight": "Clair", "themeLight": "Clair",
"themeDark": "Sombre", "themeDark": "Sombre",
"experimentsEnabled": "Activer les expériences", "experimentsEnabled": "Activer les expériences",

View File

@ -1,6 +1,6 @@
{ {
"@@locale": "it", "@@locale": "it",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",

View File

@ -1,6 +1,6 @@
{ {
"@@locale": "pl", "@@locale": "pl",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",

View File

@ -1,6 +1,6 @@
{ {
"@@locale": "pt", "@@locale": "pt",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -227,7 +227,7 @@
"radioNoPassword": "Unencrypted (No password)", "radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password", "radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiado", "copiedToClipboardNotification": "Copiado",
"editProfile": "Edit Profille", "editProfile": "Edit Profile",
"newProfile": "New Profile", "newProfile": "New Profile",
"defaultProfileName": "Alice", "defaultProfileName": "Alice",
"profileName": "Display name", "profileName": "Display name",

View File

@ -1,6 +1,6 @@
{ {
"@@locale": "ru", "@@locale": "ru",
"@@last_modified": "2022-01-18T00:38:14+01:00", "@@last_modified": "2022-01-28T19:57:41+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",

View File

@ -37,7 +37,7 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
print("runApp()"); print("runApp()");
runApp(Flwtch()); runApp(Flwtch());
sleep(Duration(seconds:1)); sleep(Duration(seconds: 1));
} }
class Flwtch extends StatefulWidget { class Flwtch extends StatefulWidget {
@ -51,13 +51,14 @@ class Flwtch extends StatefulWidget {
} }
} }
class FlwtchState extends State<Flwtch> with WindowListener, WidgetsBindingObserver { class FlwtchState extends State<Flwtch> with WindowListener {
final TextStyle biggerFont = const TextStyle(fontSize: 18); final TextStyle biggerFont = const TextStyle(fontSize: 18);
late Cwtch cwtch; late Cwtch cwtch;
late ProfileListState profs; late ProfileListState profs;
final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler'); final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler');
final MethodChannel shutdownMethodChannel = MethodChannel('im.cwtch.flwtch/shutdownClickHandler'); final MethodChannel shutdownMethodChannel = MethodChannel('im.cwtch.flwtch/shutdownClickHandler');
final MethodChannel shutdownLinuxMethodChannel = MethodChannel('im.cwtch.linux.shutdown'); final MethodChannel shutdownLinuxMethodChannel = MethodChannel('im.cwtch.linux.shutdown');
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
Future<dynamic> shutdownDirect(MethodCall call) { Future<dynamic> shutdownDirect(MethodCall call) {
@ -186,7 +187,6 @@ class FlwtchState extends State<Flwtch> with WindowListener, WidgetsBindingObser
// coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID // coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
Future<void> _externalNotificationClicked(MethodCall call) async { Future<void> _externalNotificationClicked(MethodCall call) async {
var args = jsonDecode(call.arguments); var args = jsonDecode(call.arguments);
var profile = profs.getProfile(args["ProfileOnion"])!; var profile = profs.getProfile(args["ProfileOnion"])!;
var convo = profile.contactList.getContact(args["Handle"])!; var convo = profile.contactList.getContact(args["Handle"])!;
Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages; Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
@ -231,7 +231,6 @@ class FlwtchState extends State<Flwtch> with WindowListener, WidgetsBindingObser
globalAppState.focus = false; globalAppState.focus = false;
} }
@override @override
void dispose() { void dispose() {
cwtch.Shutdown(); cwtch.Shutdown();

View File

@ -211,6 +211,7 @@ class ContactInfoState extends ChangeNotifier {
newMarker++; newMarker++;
} }
this._lastMessageTime = timestamp;
this.messageCache.addNew(profileOnion, identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash); this.messageCache.addNew(profileOnion, identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash);
this.totalMessages += 1; this.totalMessages += 1;

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/models/remoteserver.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'contact.dart'; import 'contact.dart';
@ -72,7 +73,7 @@ class ProfileInfoState extends ChangeNotifier {
List<dynamic> servers = jsonDecode(serversJson); List<dynamic> servers = jsonDecode(serversJson);
this._servers.replace(servers.map((server) { this._servers.replace(servers.map((server) {
// TODO Keys... // TODO Keys...
return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]); return RemoteServerInfoState(server["onion"], server["identifier"], server["description"], server["status"]);
})); }));
this._contacts.contacts.forEach((contact) { this._contacts.contacts.forEach((contact) {

View File

@ -1,3 +1,4 @@
import 'package:cwtch/models/remoteserver.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'contact.dart'; import 'contact.dart';
@ -33,12 +34,17 @@ class ProfileServerListState extends ChangeNotifier {
// return -1 = a first in list // return -1 = a first in list
// return 1 = b first in list // return 1 = b first in list
// online v offline // online v syncing v offline
if (a.status == "Synced" && b.status != "Synced") { if (a.status == "Synced" && b.status != "Synced") {
return -1; return -1;
} else if (a.status != "Synced" && b.status == "Synced") { } else if (a.status != "Synced" && b.status == "Synced") {
return 1; return 1;
} }
if (a.status == "Authenticated" && b.status != "Authenticated") {
return -1;
} else if (a.status != "Authenticated" && b.status == "Authenticated") {
return 1;
}
// num of groups // num of groups
if (a.groups.length > b.groups.length) { if (a.groups.length > b.groups.length) {
@ -65,30 +71,3 @@ class ProfileServerListState extends ChangeNotifier {
List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
} }
class RemoteServerInfoState extends ChangeNotifier {
final String onion;
final int identifier;
String status;
String description;
List<ContactInfoState> _groups = [];
RemoteServerInfoState({required this.onion, required this.identifier, required this.description, required this.status});
void updateDescription(String newDescription) {
this.description = newDescription;
notifyListeners();
}
void clearGroups() {
_groups = [];
}
void addGroup(ContactInfoState group) {
_groups.add(group);
notifyListeners();
}
List<ContactInfoState> get groups => _groups.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/cupertino.dart';
import 'contact.dart';
class RemoteServerInfoState extends ChangeNotifier {
final String onion;
final int identifier;
String _status;
String description;
List<ContactInfoState> _groups = [];
double syncProgress = 0;
DateTime lastPreSyncMessagTime = new DateTime(2020);
RemoteServerInfoState(this.onion, this.identifier, this.description, this._status);
void updateDescription(String newDescription) {
this.description = newDescription;
notifyListeners();
}
void clearGroups() {
_groups = [];
}
void addGroup(ContactInfoState group) {
_groups.add(group);
notifyListeners();
}
String get status => _status;
set status(String newStatus) {
_status = newStatus;
if (status == "Authenticated") {
// syncing, set lastPreSyncMessageTime
_groups.forEach((g) {
if (g.lastMessageTime.isAfter(lastPreSyncMessagTime)) {
lastPreSyncMessagTime = g.lastMessageTime;
}
});
}
notifyListeners();
}
// updateSyncProgressFor point takes a message's time, and updates the server sync progress,
// based on that point in time between the precalculated lastPreSyncMessagTime and Now
void updateSyncProgressFor(DateTime point) {
var range = lastPreSyncMessagTime.difference(DateTime.now());
var pointFromStart = lastPreSyncMessagTime.difference(point);
syncProgress = pointFromStart.inSeconds / range.inSeconds;
notifyListeners();
}
List<ContactInfoState> get groups => _groups.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}

View File

@ -183,7 +183,11 @@ ThemeData mkThemeData(Settings opaque) {
scrollbarTheme: ScrollbarThemeData(isAlwaysShown: false, thumbColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor)), scrollbarTheme: ScrollbarThemeData(isAlwaysShown: false, thumbColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor)),
tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor))), tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor))),
dialogTheme: DialogTheme( dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor, titleTextStyle: TextStyle(color: opaque.current().mainTextColor), contentTextStyle: TextStyle(color: opaque.current().mainTextColor)), backgroundColor: opaque.current().backgroundPaneColor,
titleTextStyle: TextStyle(color: opaque.current().mainTextColor),
contentTextStyle: TextStyle(
color: opaque.current().mainTextColor,
)),
textTheme: TextTheme( textTheme: TextTheme(
headline1: TextStyle(color: opaque.current().mainTextColor), headline1: TextStyle(color: opaque.current().mainTextColor),
headline2: TextStyle(color: opaque.current().mainTextColor), headline2: TextStyle(color: opaque.current().mainTextColor),

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/remoteserver.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cwtch/errorHandler.dart'; import 'package:cwtch/errorHandler.dart';

View File

@ -186,13 +186,13 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
autoFillHints: [AutofillHints.newPassword], autoFillHints: [AutofillHints.newPassword],
validator: (value) { validator: (value) {
// Password field can be empty when just updating the profile, not on creation // Password field can be empty when just updating the profile, not on creation
if (Provider.of<ProfileInfoState>(context).isEncrypted && if (Provider.of<ProfileInfoState>(context, listen: false).isEncrypted &&
Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty &&
value.isEmpty && value.isEmpty &&
usePassword) { usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty; return AppLocalizations.of(context)!.passwordErrorEmpty;
} }
if (Provider.of<ErrorHandler>(context).deleteProfileError == true) { if (Provider.of<ErrorHandler>(context, listen: false).deleteProfileError == true) {
return AppLocalizations.of(context)!.enterCurrentPasswordForDelete; return AppLocalizations.of(context)!.enterCurrentPasswordForDelete;
} }
return null; return null;

View File

@ -3,6 +3,7 @@ import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/contactlist.dart'; import 'package:cwtch/models/contactlist.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/profilelist.dart';
import 'package:cwtch/views/profileserversview.dart'; import 'package:cwtch/views/profileserversview.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/widgets/contactrow.dart'; import 'package:cwtch/widgets/contactrow.dart';
@ -154,8 +155,15 @@ class _ContactsViewState extends State<ContactsView> {
Widget _buildContactList() { Widget _buildContactList() {
final tiles = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) { final tiles = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) {
return ChangeNotifierProvider<ContactInfoState>.value(key: ValueKey(contact.profileOnion + "" + contact.onion), value: contact, builder: (_, __) => RepaintBoundary(child: ContactRow())); return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: contact),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).serverList),
],
builder: (context, child) => RepaintBoundary(child: ContactRow()),
);
}); });
final divided = ListTile.divideTiles( final divided = ListTile.divideTiles(
context: context, context: context,
tiles: tiles, tiles: tiles,

View File

@ -107,7 +107,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
items: themes.keys.map<DropdownMenuItem<String>>((String themeId) { items: themes.keys.map<DropdownMenuItem<String>>((String themeId) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: themeId, value: themeId,
child: Text("ddi_$themeId",key: Key("ddi_$themeId")),//getThemeName(context, themeId)), child: Text("ddi_$themeId", key: Key("ddi_$themeId")), //getThemeName(context, themeId)),
); );
}).toList()), }).toList()),
leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor), leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),

View File

@ -108,7 +108,12 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
} }
// Global Settings // Global Settings
actions.add(IconButton(key: Key("OpenSettingsView"), icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, splashRadius: Material.defaultSplashRadius / 2, onPressed: _pushGlobalSettings)); actions.add(IconButton(
key: Key("OpenSettingsView"),
icon: Icon(Icons.settings),
tooltip: AppLocalizations.of(context)!.tooltipOpenSettings,
splashRadius: Material.defaultSplashRadius / 2,
onPressed: _pushGlobalSettings));
// shutdown cwtch // shutdown cwtch
actions.add(IconButton(icon: Icon(Icons.close), tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip, splashRadius: Material.defaultSplashRadius / 2, onPressed: _modalShutdown)); actions.add(IconButton(icon: Icon(Icons.close), tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip, splashRadius: Material.defaultSplashRadius / 2, onPressed: _modalShutdown));
@ -191,6 +196,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
height: 20, height: 20,
), ),
CwtchPasswordField( CwtchPasswordField(
key: Key("unlockPasswordProfileElement"),
autofocus: true, autofocus: true,
controller: ctrlrPassword, controller: ctrlrPassword,
action: unlock, action: unlock,

View File

@ -1,5 +1,6 @@
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/remoteserver.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/remoteserverrow.dart'; import 'package:cwtch/widgets/remoteserverrow.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -4,6 +4,7 @@ import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/remoteserver.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/buttontextfield.dart';
import 'package:cwtch/widgets/contactrow.dart'; import 'package:cwtch/widgets/contactrow.dart';

View File

@ -5,7 +5,16 @@ import 'package:provider/provider.dart';
// Provides a styled Text Field for use in Form Widgets. // Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchButtonTextField extends StatefulWidget { class CwtchButtonTextField extends StatefulWidget {
CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true, this.labelText, this.testKey, this.onChanged,}); CwtchButtonTextField({
required this.controller,
required this.onPressed,
required this.icon,
required this.tooltip,
this.readonly = true,
this.labelText,
this.testKey,
this.onChanged,
});
final TextEditingController controller; final TextEditingController controller;
final Function()? onPressed; final Function()? onPressed;
final Function(String)? onChanged; final Function(String)? onChanged;

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/views/contactsview.dart'; import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
@ -66,6 +67,7 @@ class _ContactRowState extends State<ContactRow> {
visible: contact.isGroup && contact.status == "Authenticated", visible: contact.isGroup && contact.status == "Authenticated",
child: LinearProgressIndicator( child: LinearProgressIndicator(
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
value: Provider.of<ProfileInfoState>(context).serverList.getServer(contact.server)?.syncProgress,
)), )),
Visibility( Visibility(
visible: !Provider.of<Settings>(context).streamerMode, visible: !Provider.of<Settings>(context).streamerMode,

View File

@ -89,10 +89,7 @@ class _ProfileRowState extends State<ProfileRow> {
builder: (BuildContext buildcontext) { builder: (BuildContext buildcontext) {
return OrientationBuilder(builder: (orientationBuilderContext, orientation) { return OrientationBuilder(builder: (orientationBuilderContext, orientation) {
return MultiProvider( return MultiProvider(
providers: [ providers: [ChangeNotifierProvider<ProfileInfoState>.value(value: profile), ChangeNotifierProvider<ContactListState>.value(value: profile.contactList)],
ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
],
builder: (innercontext, widget) { builder: (innercontext, widget) {
var appState = Provider.of<AppState>(context); var appState = Provider.of<AppState>(context);
var settings = Provider.of<Settings>(context); var settings = Provider.of<Settings>(context);

View File

@ -1,6 +1,7 @@
import 'package:cwtch/main.dart'; import 'package:cwtch/main.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/remoteserver.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
import 'package:cwtch/views/addeditservers.dart'; import 'package:cwtch/views/addeditservers.dart';
import 'package:cwtch/views/remoteserverview.dart'; import 'package:cwtch/views/remoteserverview.dart';
@ -55,7 +56,13 @@ class _RemoteServerRowState extends State<RemoteServerRow> {
softWrap: true, softWrap: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(color: running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor), style: TextStyle(color: running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor),
))) ))),
Visibility(
visible: server.status == "Authenticated",
child: LinearProgressIndicator(
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
value: server.syncProgress,
)),
], ],
)), )),
]), ]),

View File

@ -132,14 +132,15 @@ static void my_application_activate(GApplication* application) {
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
// Create a specific channel for shutting down cwtch when the close button is triggered // Create a specific channel for shutting down cwtch when the close button is triggered
// We have registered the "destroy" handle above for this reason // We have registered the "destroy" handle above for this reason
FlEngine *engine = fl_view_get_engine(view); FlEngine *engine = fl_view_get_engine(view);
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
g_autoptr(FlBinaryMessenger) messenger = fl_engine_get_binary_messenger(engine); g_autoptr(FlBinaryMessenger) messenger = fl_engine_get_binary_messenger(engine);
channel = channel = fl_method_channel_new(messenger, "im.cwtch.linux.shutdown", FL_METHOD_CODEC(codec));
fl_method_channel_new(messenger,
"im.cwtch.linux.shutdown", FL_METHOD_CODEC(codec));
gtk_widget_grab_focus(GTK_WIDGET(view)); gtk_widget_grab_focus(GTK_WIDGET(view));

View File

@ -41,7 +41,7 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0 scrollable_positioned_list: ^0.2.0-nullsafety.0
file_picker: ^4.3.2 file_picker: ^4.3.2
file_picker_desktop: ^1.1.0 file_picker_desktop: ^1.1.0
url_launcher: ^6.0.12 url_launcher: ^6.0.18
desktoasts: ^0.0.2 desktoasts: ^0.0.2
window_manager: ^0.1.4 window_manager: ^0.1.4