Merge pull request 'Servers experiment and manager' (#214) from server into trunk

Reviewed-on: cwtch.im/cwtch-ui#214
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
This commit is contained in:
Dan Ballard 2021-11-02 22:26:09 +00:00
commit a9bcf13c1f
27 changed files with 1216 additions and 92 deletions

View File

@ -1 +1 @@
v1.3.1-8-g4529984-2021-11-01-22-03
v1.3.1-17-gbaddf63-2021-11-02-19-11

View File

@ -291,10 +291,61 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.rejectInvite(profile, groupHandle)
}
"SetProfileAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setProfileAttribute(profile, key, v)
}
"SetContactAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val contact = (a.get("Contact") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setContactAttribute(profile, contact, key, v)
}
"Shutdown" -> {
Cwtch.shutdownCwtch();
return Result.success()
}
"LoadServers" -> {
val password = (a.get("Password") as? String) ?: ""
Cwtch.loadServers(password)
}
"CreateServer" -> {
val password = (a.get("Password") as? String) ?: ""
val desc = (a.get("Description") as? String) ?: ""
val autostart = (a.get("Autostart") as? Boolean) ?: false
Cwtch.createServer(password, desc, autostart)
}
"DeleteServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
val password = (a.get("Password") as? String) ?: ""
Cwtch.deleteServer(serverOnion, password)
}
"LaunchServers" -> {
Cwtch.launchServers()
}
"LaunchServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
Cwtch.launchServer(serverOnion)
}
"StopServer" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
Cwtch.stopServer(serverOnion)
}
"StopServers" -> {
Cwtch.stopServers()
}
"DestroyServers" -> {
Cwtch.destroyServers()
}
"SetServerAttribute" -> {
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: ""
Cwtch.setServerAttribute(serverOnion, key, v)
}
else -> return Result.failure()
}
return Result.success()

View File

@ -1,5 +1,9 @@
import 'package:flutter/src/services/text_input.dart';
// To handle profiles that are "unencrypted" (i.e don't require a password to open) we currently create a profile with a defacto, hardcoded password.
// Details: https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
const DefaultPassword = "be gay do crime";
abstract class Cwtch {
// ignore: non_constant_identifier_names
Future<void> Start();
@ -63,6 +67,29 @@ abstract class Cwtch {
void SetGroupAttribute(String profile, String groupHandle, String key, String value);
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle);
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val);
// ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val);
// ignore: non_constant_identifier_names
void LoadServers(String password);
// ignore: non_constant_identifier_names
void CreateServer(String password, String description, bool autostart);
// ignore: non_constant_identifier_names
void DeleteServer(String serverOnion, String password);
// ignore: non_constant_identifier_names
void LaunchServers();
// ignore: non_constant_identifier_names
void LaunchServer(String serverOnion);
// ignore: non_constant_identifier_names
void StopServer(String serverOnion);
// ignore: non_constant_identifier_names
void StopServers();
// ignore: non_constant_identifier_names
void DestroyServers();
// ignore: non_constant_identifier_names
void SetServerAttribute(String serverOnion, String key, String val);
// ignore: non_constant_identifier_names
void Shutdown();

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/notification_manager.dart';
import 'package:provider/provider.dart';
@ -20,14 +21,16 @@ class CwtchNotifier {
late TorStatus torStatus;
late NotificationsManager notificationManager;
late AppState appState;
late ServerListState serverListState;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN) {
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) {
profileCN = pcn;
settings = settingsCN;
error = errorCN;
torStatus = torStatusCN;
notificationManager = notificationManagerP;
appState = appStateCN;
serverListState = serverListStateCN;
}
void handleMessage(String type, dynamic data) {
@ -59,11 +62,28 @@ class CwtchNotifier {
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
));
break;
case "NewServer":
EnvironmentConfig.debugLog("NewServer $data");
serverListState.add(
data["Onion"],
data["ServerBundle"],
data["Running"] == "true",
data["Description"],
data["Autostart"] == "true",
data["StorageType"] == "storage-password");
break;
case "ServerIntentUpdate":
EnvironmentConfig.debugLog("ServerIntentUpdate $data");
var server = serverListState.getServer(data["Identity"]);
if (server != null) {
server.setRunning(data["Intent"] == "running");
}
break;
case "GroupCreated":
// Retrieve Server Status from Cache...
String status = "";
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
if (serverInfoState != null) {
status = serverInfoState.status;
}
@ -263,7 +283,7 @@ class CwtchNotifier {
// Retrieve Server Status from Cache...
String status = "";
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
if (serverInfoState != null) {
status = serverInfoState.status;
}

View File

@ -42,6 +42,9 @@ typedef VoidFromStringStringStringStringStringFn = void Function(Pointer<Utf8>,
typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64);
typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
typedef void_from_string_string_byte_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int8);
typedef VoidFromStringStringByteFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
typedef string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length);
typedef StringFn = void Function(Pointer<Utf8> dir, int);
@ -530,6 +533,139 @@ class CwtchFfi implements Cwtch {
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val) {
var setProfileAttribute = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SetProfileAttribute");
// ignore: non_constant_identifier_names
final SetProfileAttribute = setProfileAttribute.asFunction<VoidFromStringStringStringFn>();
final u1 = profile.toNativeUtf8();
final u2 = key.toNativeUtf8();
final u3 = key.toNativeUtf8();
SetProfileAttribute(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
}
@override
// ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val) {
var setContactAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetContactAttribute");
// ignore: non_constant_identifier_names
final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringStringStringStringFn>();
final u1 = profile.toNativeUtf8();
final u2 = contact.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = key.toNativeUtf8();
SetContactAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
malloc.free(u4);
}
@override
// ignore: non_constant_identifier_names
void LoadServers(String password) {
var loadServers = library.lookup<NativeFunction<string_to_void_function>>("c_LoadServers");
// ignore: non_constant_identifier_names
final LoadServers = loadServers.asFunction<StringFn>();
final u1 = password.toNativeUtf8();
LoadServers(u1, u1.length);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void CreateServer(String password, String description, bool autostart) {
var createServer = library.lookup<NativeFunction<void_from_string_string_byte_function>>("c_CreateServer");
// ignore: non_constant_identifier_names
final CreateServer = createServer.asFunction<VoidFromStringStringByteFn>();
final u1 = password.toNativeUtf8();
final u2 = description.toNativeUtf8();
CreateServer(u1, u1.length, u2, u2.length, autostart ? 1 : 0);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void DeleteServer(String serverOnion, String password) {
var deleteServer = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteServer");
// ignore: non_constant_identifier_names
final DeleteServer = deleteServer.asFunction<VoidFromStringStringFn>();
final u1 = serverOnion.toNativeUtf8();
final u2 = password.toNativeUtf8();
DeleteServer(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void LaunchServers() {
var launchServers = library.lookup<NativeFunction<Void Function()>>("c_LaunchServers");
// ignore: non_constant_identifier_names
final LaunchServers = launchServers.asFunction<void Function()>();
LaunchServers();
}
@override
// ignore: non_constant_identifier_names
void LaunchServer(String serverOnion) {
var launchServer = library.lookup<NativeFunction<string_to_void_function>>("c_LaunchServer");
// ignore: non_constant_identifier_names
final LaunchServer = launchServer.asFunction<StringFn>();
final u1 = serverOnion.toNativeUtf8();
LaunchServer(u1, u1.length);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void StopServer(String serverOnion) {
var shutdownServer = library.lookup<NativeFunction<string_to_void_function>>("c_StopServer");
// ignore: non_constant_identifier_names
final ShutdownServer = shutdownServer.asFunction<StringFn>();
final u1 = serverOnion.toNativeUtf8();
ShutdownServer(u1, u1.length);
malloc.free(u1);
}
@override
// ignore: non_constant_identifier_names
void StopServers() {
var shutdownServers = library.lookup<NativeFunction<Void Function()>>("c_StopServers");
// ignore: non_constant_identifier_names
final ShutdownServers = shutdownServers.asFunction<void Function()>();
ShutdownServers();
}
@override
// ignore: non_constant_identifier_names
void DestroyServers() {
var destroyServers = library.lookup<NativeFunction<Void Function()>>("c_DestroyServers");
// ignore: non_constant_identifier_names
final DestroyServers = destroyServers.asFunction<void Function()>();
DestroyServers();
}
@override
// ignore: non_constant_identifier_names
void SetServerAttribute(String serverOnion, String key, String val) {
var setServerAttribute = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SetServerAttribute");
// ignore: non_constant_identifier_names
final SetServerAttribute = setServerAttribute.asFunction<VoidFromStringStringStringFn>();
final u1 = serverOnion.toNativeUtf8();
final u2 = key.toNativeUtf8();
final u3 = val.toNativeUtf8();
SetServerAttribute(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
}
@override
// ignore: non_constant_identifier_names
Future<void> Shutdown() async {

View File

@ -202,6 +202,72 @@ class CwtchGomobile implements Cwtch {
}
@override
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val) {
cwtchPlatform.invokeMethod("SetProfileAttribute", {"ProfileOnion": profile, "Key": key, "Val": val});
}
@override
// ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val) {
cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "Contact": contact, "Key": key, "Val": val});
}
@override
// ignore: non_constant_identifier_names
void LoadServers(String password) {
cwtchPlatform.invokeMethod("LoadServers", {"Password": password});
}
@override
// ignore: non_constant_identifier_names
void CreateServer(String password, String description, bool autostart) {
cwtchPlatform.invokeMethod("CreateServer", {"Password": password, "Description": description, "Autostart": autostart});
}
@override
// ignore: non_constant_identifier_names
void DeleteServer(String serverOnion, String password) {
cwtchPlatform.invokeMethod("DeleteServer", {"ServerOnion": serverOnion, "Password": password});
}
@override
// ignore: non_constant_identifier_names
void LaunchServers() {
cwtchPlatform.invokeMethod("LaunchServers", {});
}
@override
// ignore: non_constant_identifier_names
void LaunchServer(String serverOnion) {
cwtchPlatform.invokeMethod("LaunchServer", {"ServerOnion": serverOnion});
}
@override
// ignore: non_constant_identifier_names
void StopServer(String serverOnion) {
cwtchPlatform.invokeMethod("StopServer", {"ServerOnion": serverOnion});
}
@override
// ignore: non_constant_identifier_names
void StopServers() {
cwtchPlatform.invokeMethod("StopServers", {});
}
@override
// ignore: non_constant_identifier_names
void DestroyServers() {
cwtchPlatform.invokeMethod("DestroyServers", {});
}
@override
// ignore: non_constant_identifier_names
void SetServerAttribute(String serverOnion, String key, String val) {
cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val});
}
@override
Future<void> Shutdown() async {
print("gomobile.dart Shutdown");
cwtchPlatform.invokeMethod("Shutdown", {});

View File

@ -1,6 +1,27 @@
{
"@@locale": "de",
"@@last_modified": "2021-09-21T23:09:19+02:00",
"@@last_modified": "2021-11-02T23:03:43+01:00",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
@ -9,10 +30,9 @@
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
"labelFilename": "Filename",
"downloadFileButton": "Download",
"downloadFileButton": "Herunterladen",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact onions",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",

View File

@ -1,6 +1,27 @@
{
"@@locale": "en",
"@@last_modified": "2021-09-21T23:09:19+02:00",
"@@last_modified": "2021-11-02T23:03:43+01:00",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
@ -12,7 +33,6 @@
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact onions",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Send this address to people you want to connect with",

View File

@ -1,6 +1,27 @@
{
"@@locale": "es",
"@@last_modified": "2021-09-21T23:09:19+02:00",
"@@last_modified": "2021-11-02T23:03:43+01:00",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
@ -12,7 +33,6 @@
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact onions",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",

View File

@ -1,18 +1,38 @@
{
"@@locale": "fr",
"@@last_modified": "2021-09-21T23:09:19+02:00",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
"messageFileOffered": "Contact is offering to send you a file",
"messageFileSent": "You sent a file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
"labelFilename": "Filename",
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "Si elle est activée, cette option donne un rendu visuel plus privé à l'application pour la diffusion ou la présentation, par exemple en masquant les profils et les contacts.",
"@@last_modified": "2021-11-02T23:03:43+01:00",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profils",
"descriptionStreamerMode": "Si elle est activée, cette option donne un rendu visuel plus privé à l'application pour la diffusion en direct ou la présentation, par exemple, en masquant profil et adresses de contacts.",
"descriptionFileSharing": "L'expérience de partage de fichiers vous permet d'envoyer et de recevoir des fichiers à partir de contacts et de groupes Cwtch. Notez que si vous partagez un fichier avec un groupe, les membres de ce groupe se connecteront avec vous directement via Cwtch pour le télécharger.",
"settingFileSharing": "Partage de fichiers",
"tooltipSendFile": "Envoyer le fichier",
"messageFileOffered": "Contact vous propose de vous envoyer un fichier",
"messageFileSent": "Vous avez envoyé un fichier",
"messageEnableFileSharing": "Activez l'expérience de partage de fichiers pour afficher ce message.",
"labelFilesize": "Taille",
"labelFilename": "Nom de fichier",
"downloadFileButton": "Télécharger",
"openFolderButton": "Ouvrir le dossier",
"retrievingManifestMessage": "Récupération des informations sur le fichier...",
"streamerModeLabel": "Mode Streamer\/Présentation",
"archiveConversation": "Archiver cette conversation",
"profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.",

View File

@ -1,10 +1,31 @@
{
"@@locale": "it",
"@@last_modified": "2021-09-21T23:09:19+02:00",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
"messageFileOffered": "Contact is offering to send you a file",
"@@last_modified": "2021-11-02T23:03:43+01:00",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "L'esperimento di condivisione dei file ti consente di inviare e ricevere file dai contatti e dai gruppi di Cwtch. Tieni presente che la condivisione di un file con un gruppo farà sì che i membri di quel gruppo si colleghino con te direttamente su Cwtch per scaricarlo.",
"settingFileSharing": "Condivisione file",
"tooltipSendFile": "Invia file",
"messageFileOffered": "Il contatto offre l'invio di un file",
"messageFileSent": "You sent a file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
@ -12,7 +33,6 @@
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact onions",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",

View File

@ -1,20 +1,40 @@
{
"@@locale": "pl",
"@@last_modified": "2021-09-21T23:09:19+02:00",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
"messageFileOffered": "Contact is offering to send you a file",
"messageFileSent": "You sent a file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size",
"labelFilename": "Filename",
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact onions",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"@@last_modified": "2021-11-02T23:03:43+01:00",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profile",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "Eksperyment udostępniania plików pozwala na wysyłanie i odbieranie plików od kontaktów i grup Cwtch. Zauważ, że udostępnienie pliku grupie spowoduje, że członkowie tej grupy połączą się z Tobą bezpośrednio przez Cwtch, aby go pobrać.",
"settingFileSharing": "Udostępnianie plików",
"tooltipSendFile": "Wyślij plik",
"messageFileOffered": "Kontakt proponuje wysłanie Ci pliku",
"messageFileSent": "Plik został wysłany",
"messageEnableFileSharing": "Włącz eksperyment udostępniania plików, aby wyświetlić tę wiadomość.",
"labelFilesize": "Rozmiar",
"labelFilename": "Nazwa pliku",
"downloadFileButton": "Pobierz",
"openFolderButton": "Otwórz folder",
"retrievingManifestMessage": "Pobieranie informacji o pliku...",
"streamerModeLabel": "Tryb streamera\/prezentacji",
"archiveConversation": "Zarchiwizuj tę rozmowę",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"addPeerTab": "Add a contact",
"addPeer": "Add Contact",
@ -27,7 +47,7 @@
"dontSavePeerHistory": "Delete History",
"unblockBtn": "Unblock Contact",
"blockUnknownLabel": "Block Unknown Contacts",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"blockUnknownConnectionsEnabledDescription": "Połączenia od nieznanych kontaktów są blokowane. Można to zmienić w Ustawieniach",
"networkStatusConnecting": "Connecting to network and contacts...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",

View File

@ -1,6 +1,27 @@
{
"@@locale": "pt",
"@@last_modified": "2021-09-21T23:09:19+02:00",
"@@last_modified": "2021-11-02T23:03:43+01:00",
"copyAddress": "Copy Address",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description",
"serverAddress": "Server Address",
"editServerTitle": "Edit Server",
"addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File",
@ -12,7 +33,6 @@
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact onions",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Send this address to contacts you want to connect with",

View File

@ -17,6 +17,7 @@ import 'cwtch/cwtch.dart';
import 'cwtch/cwtchNotifier.dart';
import 'licenses.dart';
import 'model.dart';
import 'models/servers.dart';
import 'views/profilemgrview.dart';
import 'views/splashView.dart';
import 'dart:io' show Platform, exit;
@ -27,6 +28,7 @@ var globalSettings = Settings(Locale("en", ''), OpaqueDark());
var globalErrorHandler = ErrorHandler();
var globalTorStatus = TorStatus();
var globalAppState = AppState();
var globalServersList = ServerListState();
void main() {
print("Cwtch version: ${EnvironmentConfig.BUILD_VER} built on: ${EnvironmentConfig.BUILD_DATE}");
@ -62,13 +64,13 @@ class FlwtchState extends State<Flwtch> {
shutdownMethodChannel.setMethodCallHandler(modalShutdown);
print("initState: creating cwtchnotifier, ffi");
if (Platform.isAndroid) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
cwtch = CwtchGomobile(cwtchNotifier);
} else if (Platform.isLinux) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList);
cwtch = CwtchFfi(cwtchNotifier);
} else {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
cwtch = CwtchFfi(cwtchNotifier);
}
print("initState: invoking cwtch.Start()");
@ -82,6 +84,7 @@ class FlwtchState extends State<Flwtch> {
ChangeNotifierProvider<AppState> getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState);
Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
ChangeNotifierProvider<ServerListState> getServerListStateProvider() => ChangeNotifierProvider.value(value: globalServersList);
@override
Widget build(BuildContext context) {
@ -94,6 +97,7 @@ class FlwtchState extends State<Flwtch> {
getErrorHandlerProvider(),
getTorStatusProvider(),
getAppStateProvider(),
getServerListStateProvider(),
],
builder: (context, widget) {
return Consumer2<Settings, AppState>(

View File

@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/models/profileservers.dart';
////////////////////
/// UI State ///
@ -207,7 +207,7 @@ class ContactListState extends ChangeNotifier {
class ProfileInfoState extends ChangeNotifier {
ContactListState _contacts = ContactListState();
ServerListState _servers = ServerListState();
ProfileServerListState _servers = ProfileServerListState();
final String onion;
String _nickname = "";
String _imagePath = "";
@ -267,7 +267,7 @@ class ProfileInfoState extends ChangeNotifier {
List<dynamic> servers = jsonDecode(serversJson);
this._servers.replace(servers.map((server) {
// TODO Keys...
return ServerInfoState(onion: server["onion"], status: server["status"]);
return RemoteServerInfoState(onion: server["onion"], status: server["status"]);
}));
notifyListeners();
}
@ -316,7 +316,7 @@ class ProfileInfoState extends ChangeNotifier {
}
ContactListState get contactList => this._contacts;
ServerListState get serverList => this._servers;
ProfileServerListState get serverList => this._servers;
@override
void dispose() {

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class ProfileServerListState extends ChangeNotifier {
List<RemoteServerInfoState> _servers = [];
void replace(Iterable<RemoteServerInfoState> newServers) {
_servers.clear();
_servers.addAll(newServers);
notifyListeners();
}
RemoteServerInfoState? getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null;
}
void updateServerCache(String onion, String status) {
int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_servers[idx] = RemoteServerInfoState(onion: onion, status: status);
} else {
print("Tried to update server cache without a starting state...this is probably an error");
}
notifyListeners();
}
List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
}
class RemoteServerInfoState extends ChangeNotifier {
final String onion;
final String status;
RemoteServerInfoState({required this.onion, required this.status});
}

View File

@ -9,17 +9,27 @@ class ServerListState extends ChangeNotifier {
notifyListeners();
}
void clear() {
_servers.clear();
}
ServerInfoState? getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null;
}
void updateServerCache(String onion, String status) {
void add(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
print("servers.add desc:$description autostart: $autoStart");
_servers.add(ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted));
notifyListeners();
}
void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_servers[idx] = ServerInfoState(onion: onion, status: status);
_servers[idx] = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted);
} else {
print("Tried to update server cache without a starting state...this is probably an error");
print("Tried to update server list without a starting state...this is probably an error");
}
notifyListeners();
}
@ -29,8 +39,27 @@ class ServerListState extends ChangeNotifier {
}
class ServerInfoState extends ChangeNotifier {
final String onion;
final String status;
String onion;
String serverBundle;
String description;
bool running;
bool autoStart;
bool isEncrypted;
ServerInfoState({required this.onion, required this.status});
ServerInfoState({required this.onion, required this.serverBundle, required this.running, required this.description, required this.autoStart, required this.isEncrypted});
void setAutostart(bool val) {
autoStart = val;
notifyListeners();
}
void setRunning(bool val) {
running = val;
notifyListeners();
}
void setDescription(String val) {
description = val;
notifyListeners();
}
}

View File

@ -9,6 +9,7 @@ import 'opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
const TapirGroupsExperiment = "tapir-groups-experiment";
const ServerManagementExperiment = "servers-experiment";
const FileSharingExperiment = "filesharing";
enum DualpaneMode {

View File

@ -4,7 +4,7 @@ import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/errorHandler.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/settings.dart';
import 'package:cwtch/widgets/buttontextfield.dart';
import 'package:cwtch/widgets/cwtchlabel.dart';
@ -197,7 +197,7 @@ class _AddContactViewState extends State<AddContactView> {
},
isExpanded: true, // magic property
value: server,
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((RemoteServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(
@ -240,8 +240,8 @@ class _AddContactViewState extends State<AddContactView> {
/// TODO Manage Servers Tab
Widget manageServersTab() {
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) {
return ChangeNotifierProvider<ServerInfoState>.value(
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((RemoteServerInfoState server) {
return ChangeNotifierProvider<RemoteServerInfoState>.value(
value: server,
child: ListTile(
title: Text(server.onion),

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/model.dart';
@ -106,6 +107,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
labelText: AppLocalizations.of(context)!.yourDisplayName,
validator: (value) {
if (value.isEmpty) {
// TODO l10n ize
return "Please enter a display name";
}
return null;
@ -287,32 +289,19 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text);
Navigator.of(context).pop();
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, "be gay do crime");
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, DefaultPassword);
Navigator.of(context).pop();
}
} else {
// Profile Editing
if (ctrlrPass.value.text.isEmpty) {
// Don't update password, only update name
final event = {
"EventType": "SetAttribute",
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
};
final json = jsonEncode(event);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, json);
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
Navigator.of(context).pop();
} else {
// At this points passwords have been validated to be the same and not empty
// Update both password and name, even if name hasn't been changed...
final updateNameEvent = {
"EventType": "SetAttribute",
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
};
final updateNameEventJson = jsonEncode(updateNameEvent);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, updateNameEventJson);
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
final updatePasswordEvent = {
"EventType": "ChangePassword",
"Data": {"Password": ctrlrOldPass.text, "NewPassword": ctrlrPass.text}

View File

@ -0,0 +1,330 @@
import 'dart:convert';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/cwtchlabel.dart';
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/textfield.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../errorHandler.dart';
import '../main.dart';
import '../config.dart';
/// Pane to add or edit a server
class AddEditServerView extends StatefulWidget {
const AddEditServerView();
@override
_AddEditServerViewState createState() => _AddEditServerViewState();
}
class _AddEditServerViewState extends State<AddEditServerView> {
final _formKey = GlobalKey<FormState>();
final ctrlrDesc = TextEditingController(text: "");
final ctrlrOldPass = TextEditingController(text: "");
final ctrlrPass = TextEditingController(text: "");
final ctrlrPass2 = TextEditingController(text: "");
final ctrlrOnion = TextEditingController(text: "");
late bool usePassword;
//late bool deleted;
@override
void initState() {
super.initState();
var serverInfoState = Provider.of<ServerInfoState>(context, listen: false);
ctrlrOnion.text = serverInfoState.onion;
usePassword = serverInfoState.isEncrypted;
if (serverInfoState.description.isNotEmpty) {
ctrlrDesc.text = serverInfoState.description;
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle),
),
body: _buildSettingsList(),
);
}
void _handleSwitchPassword(bool? value) {
setState(() {
usePassword = value!;
});
}
Widget _buildSettingsList() {
return Consumer2<ServerInfoState, Settings>(builder: (context, serverInfoState, settings, child) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Form(
key: _formKey,
child: Container(
margin: EdgeInsets.fromLTRB(30, 0, 30, 10),
padding: EdgeInsets.fromLTRB(20, 0 , 20, 10),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Onion
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
SizedBox(
height: 20,
),
SelectableText(
serverInfoState.onion
)
])),
// Description
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
Text(AppLocalizations.of(context)!.serverDescriptionDescription),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrDesc,
labelText: "Description",
autofocus: false,
)
]),
SizedBox(
height: 20,
),
// Enabled
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: SwitchListTile(
title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription),
value: serverInfoState.running,
onChanged: (bool value) {
serverInfoState.setRunning(value);
if (value) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor()),
)),
// Auto start
SwitchListTile(
title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription),
value: serverInfoState.autoStart,
onChanged: (bool value) {
serverInfoState.setAutostart(value);
if (! serverInfoState.onion.isEmpty) {
Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()),
),
// ***** Password *****
Visibility(
visible: serverInfoState.onion.isEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
SizedBox(
height: 20,
),
Checkbox(
value: usePassword,
fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor()),
activeColor: settings.current().defaultButtonActiveColor(),
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context)!.radioUsePassword,
style: TextStyle(color: settings.current().mainTextColor()),
),
SizedBox(
height: 20,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
usePassword ? AppLocalizations.of(context)!.encryptedProfileDescription : AppLocalizations.of(context)!.plainProfileDescription,
textAlign: TextAlign.center,
))
])),
SizedBox(
height: 20,
),
Visibility(
// Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
visible: serverInfoState.onion.isEmpty && usePassword,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Visibility(
visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrOldPass,
autoFillHints: [AutofillHints.newPassword],
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.isEncrypted &&
serverInfoState.onion.isEmpty &&
value.isEmpty &&
usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (Provider.of<ErrorHandler>(context).deleteProfileError == true) {
return AppLocalizations.of(context)!.enterCurrentPasswordForDelete;
}
return null;
},
),
SizedBox(
height: 20,
),
])),
CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass2.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
},
),
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass2,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
}),
]),
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
child: Text(
serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton,
textAlign: TextAlign.center,
),
),
),
],
),
// ***** END Password *****
]))))));
});
});
}
void _createPressed() {
// This will run all the validations in the form including
// checking that display name is not empty, and an actual check that the passwords
// match (and are provided if the user has requested an encrypted profile).
if (_formKey.currentState!.validate()) {
if (usePassword) {
Provider
.of<FlwtchState>(context, listen: false)
.cwtch
.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
} else {
Provider
.of<FlwtchState>(context, listen: false)
.cwtch
.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
}
Navigator.of(context).pop();
}
}
void _savePressed() {
var server = Provider.of<ServerInfoState>(context, listen: false);
Provider.of<FlwtchState>(context, listen: false)
.cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
server.setDescription(ctrlrDesc.text);
if (_formKey.currentState!.validate()) {
// TODO support change password
}
Navigator.of(context).pop();
}
}

View File

@ -5,6 +5,7 @@ import 'package:cwtch/widgets/contactrow.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:cwtch/widgets/textfield.dart';
import 'package:cwtch/widgets/tor_icon.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../settings.dart';
@ -103,9 +104,19 @@ class _ContactsViewState extends State<ContactsView> {
if (Provider.of<Settings>(context).blockUnknownConnections) {
actions.add(Tooltip(message: AppLocalizations.of(context)!.blockUnknownConnectionsEnabledDescription, child: Icon(CwtchIcons.block_unknown)));
}
actions.add(
IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
);
// Copy profile onion
actions.add(IconButton(
icon: Icon(CwtchIcons.address_copy_2),
tooltip: AppLocalizations.of(context)!.copyAddress,
onPressed: () {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
}));
// TODO servers
// Search contacts
actions.add(IconButton(
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/servers.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
@ -188,6 +189,24 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.settingServersDescription),
value: settings.isExperimentEnabled(ServerManagementExperiment),
onChanged: (bool value) {
Provider.of<ServerListState>(context, listen: false).clear();
if (value) {
settings.enableExperiment(ServerManagementExperiment);
} else {
settings.disableExperiment(ServerManagementExperiment);
}
// Save Settings...
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.dns_24px, color: settings.current().mainTextColor()),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingFileSharing, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionFileSharing),

View File

@ -17,6 +17,7 @@ import '../model.dart';
import '../torstatus.dart';
import 'addeditprofileview.dart';
import 'globalsettingsview.dart';
import 'serversview.dart';
class ProfileMgrView extends StatefulWidget {
ProfileMgrView();
@ -56,12 +57,14 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
SizedBox(
width: 10,
),
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
Expanded(child: Text(MediaQuery.of(context).size.width > 600 ?
AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort,
style: TextStyle(color: settings.current().mainTextColor())))
]),
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
onPressed: _pushAddProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
@ -95,6 +98,11 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
onPressed: _modalUnlockProfiles,
));
// Servers
if (Provider.of<Settings>(context).isExperimentEnabled(ServerManagementExperiment)) {
actions.add(IconButton(icon: Icon(CwtchIcons.dns_black_24dp), tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers));
}
// Global Settings
actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings));
@ -119,6 +127,17 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
));
}
void _pushServers() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
child: ServersView(),
);
},
));
}
void _pushTorStatus() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
@ -130,7 +149,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
));
}
void _pushAddEditProfile({onion: ""}) {
void _pushAddProfile({onion: ""}) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
@ -216,9 +235,9 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
).toList();
if (tiles.isEmpty) {
return const Center(
child: const Text(
"Please create or unlock a profile to begin!",
return Center(
child: Text(
AppLocalizations.of(context)!.unlockProfileTip,
textAlign: TextAlign.center,
));
}

152
lib/views/serversview.dart Normal file
View File

@ -0,0 +1,152 @@
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/views/addeditservers.dart';
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/serverrow.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/torstatus.dart';
import 'package:cwtch/widgets/tor_icon.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../cwtch_icons_icons.dart';
import '../main.dart';
import '../settings.dart';
///
class ServersView extends StatefulWidget {
@override
_ServersView createState() => _ServersView();
}
class _ServersView extends State<ServersView> {
final ctrlrPassword = TextEditingController();
@override
void dispose() {
ctrlrPassword.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort),
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddServer,
tooltip: AppLocalizations.of(context)!.addServerTooltip,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
),
),
body: Consumer<ServerListState>(
builder: (context, svrs, child) {
final tiles = svrs.servers.map((ServerInfoState server) {
return ChangeNotifierProvider<ServerInfoState>.value(
value: server,
builder: (context, child) => RepaintBoundary(child: ServerRow()),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return Center(
child: Text(
AppLocalizations.of(context)!.unlockServerTip,
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
},
));
}
List<Widget> getActions() {
List<Widget> actions = new List<Widget>.empty(growable: true);
// Unlock Profiles
actions.add(IconButton(
icon: Icon(CwtchIcons.lock_open_24px),
color: Provider.of<ServerListState>(context).servers.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor() : Provider.of<Settings>(context).theme.mainTextColor(),
tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
onPressed: _modalUnlockServers,
));
return actions;
}
void _modalUnlockServers() {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context)!.enterServerPassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
autofocus: true,
controller: ctrlrPassword,
action: unlock,
validator: (value) {},
),
SizedBox(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock),
onPressed: () {
unlock(ctrlrPassword.value.text);
},
)),
Spacer()
]),
],
))),
)));
});
}
void unlock(String password) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LoadServers(password);
ctrlrPassword.text = "";
Navigator.pop(context);
}
void _pushAddServer() {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [ChangeNotifierProvider<ServerInfoState>(
create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true),
)],
child: AddEditServerView(),
);
},
));
}
}

View File

@ -61,7 +61,7 @@ class _ProfileRowState extends State<ProfileRow> {
tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
_pushEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
},
)
],
@ -100,7 +100,7 @@ class _ProfileRowState extends State<ProfileRow> {
);
}
void _pushAddEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(

View File

@ -0,0 +1,94 @@
import 'package:cwtch/main.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/views/addeditservers.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../cwtch_icons_icons.dart';
import '../model.dart';
import '../settings.dart';
class ServerRow extends StatefulWidget {
@override
_ServerRowState createState() => _ServerRowState();
}
class _ServerRowState extends State<ServerRow> {
@override
Widget build(BuildContext context) {
var server = Provider.of<ServerInfoState>(context);
return Card(clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(0.0),
child: InkWell(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(6.0), //border size
child: Icon(CwtchIcons.dns_24px,
color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
size: 64)
),
Expanded(
child: Column(
children: [
Text(
server.description,
semanticsLabel: server.description,
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
server.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
)))
],
)),
// Copy server button
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context)!.copyAddress,
icon: Icon(CwtchIcons.address_copy_2, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
Clipboard.setData(new ClipboardData(text: server.serverBundle));
},
),
// Edit button
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context)!.editServerTitle,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
_pushEditServer(server);
},
)
])));
}
void _pushEditServer(ServerInfoState server ) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [ChangeNotifierProvider<ServerInfoState>(
create: (_) => server,
)],
child: AddEditServerView(),
);
},
));
}
}