diff --git a/.drone.yml b/.drone.yml index 7079ca5e..72ba2bc3 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,7 +8,7 @@ clone: steps: - name: clone - image: cirrusci/flutter:2.5.3 + image: cirrusci/flutter:2.8.0 environment: buildbot_key_b64: from_secret: buildbot_key_b64 @@ -24,7 +24,7 @@ steps: - git checkout $DRONE_COMMIT - name: fetch - image: cirrusci/flutter:2.5.3 + image: cirrusci/flutter:2.8.0 volumes: - name: deps path: /root/.pub-cache @@ -47,7 +47,7 @@ steps: # #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting - name: build-linux - image: openpriv/flutter-desktop:linux-fstable-2.5.3 + image: openpriv/flutter-desktop:linux-fstable-2.8.0 volumes: - name: deps path: /root/.pub-cache @@ -61,7 +61,7 @@ steps: - rm -r cwtch - name: test-build-android - image: cirrusci/flutter:2.5.3 + image: cirrusci/flutter:2.8.0 when: event: pull_request volumes: @@ -71,7 +71,7 @@ steps: - flutter build apk --debug - name: build-android - image: cirrusci/flutter:2.5.3 + image: cirrusci/flutter:2.8.0 when: event: push environment: @@ -95,7 +95,7 @@ steps: #- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android - name: widget-tests - image: cirrusci/flutter:2.5.3 + image: cirrusci/flutter:2.8.0 volumes: - name: deps path: /root/.pub-cache @@ -174,7 +174,7 @@ clone: steps: - name: clone - image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 + image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1 environment: buildbot_key_b64: from_secret: buildbot_key_b64 @@ -192,7 +192,7 @@ steps: - git checkout $Env:DRONE_COMMIT - name: fetch - image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 + image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1 commands: - powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip" - powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }" @@ -201,7 +201,7 @@ steps: - .\fetch-libcwtch-go.ps1 - name: build-windows - image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 + image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1 commands: - flutter pub get - $Env:version += type .\VERSION @@ -257,7 +257,7 @@ steps: - move *.sha512 deploy\$Env:builddir - name: deploy-windows - image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 + image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1 when: event: push status: [ success ] diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index dcda997e..76fba53c 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2021-12-11-02-00-v1.5.0-9-gaa102bd \ No newline at end of file +2021-12-18-20-32-v1.5.1 \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 8f72ef13..d7e07e7c 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2021-12-11-07-00-v1.5.0-9-gaa102bd \ No newline at end of file +2021-12-19-01-32-v1.5.1 \ No newline at end of file diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index e8c737ad..ed5283d8 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -140,7 +140,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val data = JSONObject(evt.Data); val tempFile = data.getString("TempFile"); val fileKey = data.getString("FileKey"); - if (tempFile != "") { + if (tempFile != "" && tempFile != data.getString("FilePath")) { val filePath = data.getString("FilePath"); Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath); val sourcePath = Paths.get(tempFile); diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 20940c89..2663f54c 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -22,6 +22,10 @@ import io.flutter.plugin.common.ErrorLogResult import org.json.JSONObject import java.util.concurrent.TimeUnit +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption import android.net.Uri import android.provider.DocumentsContract @@ -57,9 +61,11 @@ class MainActivity: FlutterActivity() { // "Download to..." prompt extra arguments private val FILEPICKER_REQUEST_CODE = 234 + private val PREVIEW_EXPORT_REQUEST_CODE = 235 private var dlToProfile = "" private var dlToHandle = "" private var dlToFileKey = "" + private var exportFromPath = "" // handles clicks received from outside the app (ie, notifications) override fun onNewIntent(intent: Intent) { @@ -102,6 +108,20 @@ class MainActivity: FlutterActivity() { "manifestpath" to manifestPath, "filekey" to this.dlToFileKey )), ErrorLogResult(""));//placeholder; this Result is never actually invoked + } else if (requestCode == PREVIEW_EXPORT_REQUEST_CODE) { + val targetPath = intent!!.getData().toString() + var srcFile = File(this.exportFromPath) + Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file") + val sourcePath = Paths.get(this.exportFromPath); + val targetUri = Uri.parse(targetPath); + val os = this.applicationContext.getContentResolver().openOutputStream(targetUri); + val bytesWritten = Files.copy(sourcePath, os); + Log.d("MainActivity:PREVIEW_EXPORT", "copied " + bytesWritten.toString() + " bytes"); + if (bytesWritten != 0L) { + os?.flush(); + os?.close(); + //Files.delete(sourcePath); + } } } @@ -170,6 +190,26 @@ class MainActivity: FlutterActivity() { } startActivityForResult(intent, FILEPICKER_REQUEST_CODE) return + } else if (call.method == "ExportPreviewedFile") { + this.exportFromPath = argmap["Path"] ?: "" + val suggestion = argmap["FileName"] ?: "filename.ext" + var imgType = "jpeg" + if (suggestion.endsWith("png")) { + imgType = "png" + } else if (suggestion.endsWith("webp")) { + imgType = "webp" + } else if (suggestion.endsWith("bmp")) { + imgType = "bmp" + } else if (suggestion.endsWith("gif")) { + imgType = "gif" + } + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "image/" + imgType + putExtra(Intent.EXTRA_TITLE, suggestion) + } + startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE) + return } // ...otherwise fallthru to a normal ffi method call (and return the result using the result callback) diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index a2606b27..46e4c754 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -15,7 +15,9 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void LoadProfiles(String pass); // ignore: non_constant_identifier_names - void DeleteProfile(String onion, String pass); + void DeleteProfile(String profile, String pass); + // ignore: non_constant_identifier_names + void ChangePassword(String profile, String pass, String newpass, String newpassAgain); // ignore: non_constant_identifier_names void ResetTor(); @@ -49,12 +51,16 @@ abstract class Cwtch { void ShareFile(String profile, int handle, String filepath); // ignore: non_constant_identifier_names void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey); + // android-only // ignore: non_constant_identifier_names void CreateDownloadableFile(String profile, int handle, String filenameSuggestion, String filekey); // ignore: non_constant_identifier_names void CheckDownloadStatus(String profile, String fileKey); // ignore: non_constant_identifier_names void VerifyOrResumeDownload(String profile, int handle, String filekey); + // android-only + // ignore: non_constant_identifier_names + void ExportPreviewedFile(String sourceFile, String suggestion); // ignore: non_constant_identifier_names void ArchiveConversation(String profile, int handle); @@ -94,5 +100,8 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void Shutdown(); + // non-ffi + String defaultDownloadPath(); + void dispose(); } diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index c52c3199..c284d08d 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -141,6 +141,7 @@ class CwtchNotifier { var timestamp = DateTime.tryParse(data['TimestampReceived'])!; var senderHandle = data['RemotePeer']; var senderImage = data['Picture']; + var isAuto = data['Auto'] == "true"; // We might not have received a contact created for this contact yet... // In that case the **next** event we receive will actually update these values... @@ -151,7 +152,7 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data["Data"]); // We only ever see messages from authenticated peers. // If the contact is marked as offline then override this - can happen when the contact is removed from the front @@ -197,10 +198,11 @@ class CwtchNotifier { var senderImage = data['Picture']; var timestampSent = DateTime.tryParse(data['TimestampSent'])!; var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages; + var isAuto = data['Auto'] == "true"; // Only bother to do anything if we know about the group and the provided index is greater than our current total... if (currentTotal != null && idx >= currentTotal) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"]); //if not currently open if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { @@ -228,7 +230,10 @@ class CwtchNotifier { } break; case "SendMessageToPeerError": - // Ignore + // Ignore dealt with by IndexedFailure + break; + case "SendMessageToGroupError": + // Ignore dealt with by IndexedFailure break; case "IndexedFailure": var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]); @@ -339,6 +344,11 @@ class CwtchNotifier { EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}"); } break; + case "ManifestSizeReceived": + if (!profileCN.getProfile(data["ProfileOnion"])!.downloadActive(data["FileKey"])) { + profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], 0, 1); + } + break; case "ManifestSaved": profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]); break; diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 37ea80a8..ebd444cb 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -115,13 +115,13 @@ class CwtchFfi implements Cwtch { } CwtchFfi(CwtchNotifier _cwtchNotifier) { - String library_path = getLibraryPath(); - if (library_path == UNSUPPORTED_OS) { + String libraryPath = getLibraryPath(); + if (libraryPath == UNSUPPORTED_OS) { print("OS ${Platform.operatingSystem} not supported by cwtch/ffi"); // emergency, ideally the app stays on splash and just posts the error till user closes exit(0); } - library = DynamicLibrary.open(library_path); + library = DynamicLibrary.open(libraryPath); cwtchNotifier = _cwtchNotifier; } @@ -402,6 +402,11 @@ class CwtchFfi implements Cwtch { // android only - do nothing } + // ignore: non_constant_identifier_names + void ExportPreviewedFile(String sourceFile, String suggestion) { + // android only - do nothing + } + @override // ignore: non_constant_identifier_names void CheckDownloadStatus(String profileOnion, String fileKey) { @@ -693,6 +698,13 @@ class CwtchFfi implements Cwtch { } @override + String defaultDownloadPath() { + Map envVars = Platform.environment; + return path.join(envVars[Platform.isWindows ? 'UserProfile' : 'HOME']!, "Downloads"); + } + + @override + // ignore: non_constant_identifier_names Future GetMessageByID(String profile, int handle, int index) async { var getMessageC = library.lookup>("c_GetMessageByID"); // ignore: non_constant_identifier_names @@ -704,4 +716,21 @@ class CwtchFfi implements Cwtch { malloc.free(utf8profile); return jsonMessage; } + + @override + // ignore: non_constant_identifier_names + void ChangePassword(String profile, String pass, String newpass, String newpassAgain) { + var changePasswordC = library.lookup>("c_ChangePassword"); + // ignore: non_constant_identifier_names + final ChangePasswordFn = changePasswordC.asFunction(); + final utf8profile = profile.toNativeUtf8(); + final utf8pass = pass.toNativeUtf8(); + final utf8newpass = newpass.toNativeUtf8(); + final utf8newpasssagain = newpassAgain.toNativeUtf8(); + ChangePasswordFn(utf8profile, utf8profile.length, utf8pass, utf8pass.length, utf8newpass, utf8newpass.length, utf8newpasssagain, utf8newpasssagain.length); + malloc.free(utf8profile); + malloc.free(utf8pass); + malloc.free(utf8newpass); + malloc.free(utf8newpasssagain); + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 1b4c7fd0..29bf082f 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -28,6 +28,7 @@ class CwtchGomobile implements Cwtch { late Future androidLibraryDir; late Future androidHomeDirectory; + String androidHomeDirectoryStr = ""; late CwtchNotifier cwtchNotifier; CwtchGomobile(CwtchNotifier _cwtchNotifier) { @@ -44,7 +45,8 @@ class CwtchGomobile implements Cwtch { // ignore: non_constant_identifier_names Future Start() async { print("gomobile.dart: Start()..."); - var cwtchDir = path.join((await androidHomeDirectory).path, ".cwtch"); + androidHomeDirectoryStr = (await androidHomeDirectory).path; + var cwtchDir = path.join(androidHomeDirectoryStr, ".cwtch"); if (EnvironmentConfig.BUILD_VER == dev_version) { cwtchDir = path.join(cwtchDir, "dev"); } @@ -147,6 +149,14 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filename": filenameSuggestion, "filekey": filekey}); } + // ignore: non_constant_identifier_names + void ExportPreviewedFile(String sourceFile, String suggestion) { + cwtchPlatform.invokeMethod("ExportPreviewedFile", { + "Path": sourceFile, + "FileName": suggestion, + }); + } + @override // ignore: non_constant_identifier_names void CheckDownloadStatus(String profileOnion, String fileKey) { @@ -269,4 +279,14 @@ class CwtchGomobile implements Cwtch { void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) { cwtchPlatform.invokeMethod("SetMessageAttribute", {"ProfileOnion": profile, "conversation": conversation, "Channel": channel, "Message": message, "Key": key, "Val": val}); } + + @override + String defaultDownloadPath() { + return this.androidHomeDirectoryStr; + } + + @override + void ChangePassword(String profile, String pass, String newpass, String newpassAgain) { + cwtchPlatform.invokeMethod("ChangePassword", {"ProfileOnion": profile, "OldPass": pass, "NewPass": newpass, "NewPassAgain": newpassAgain}); + } } diff --git a/lib/errorHandler.dart b/lib/errorHandler.dart index a54094c7..156b708a 100644 --- a/lib/errorHandler.dart +++ b/lib/errorHandler.dart @@ -6,12 +6,17 @@ class ErrorHandler extends ChangeNotifier { // Add Contact Specific Errors... static const String addContactErrorPrefix = "addcontact"; + static const String changePasswordErrorPrefix = "changepassword"; static const String invalidImportStringErrorType = "invalid_import_string"; static const String contactAlreadyExistsErrorType = "contact_already_exists"; bool invalidImportStringError = false; bool contactAlreadyExistsError = false; bool explicitAddContactSuccess = false; + // ChangePassword + bool changePasswordError = false; + bool explicitChangePasswordSuccess = false; + // Import Bundle Specific Errors static const String importBundleErrorPrefix = "importBundle"; bool importBundleError = false; @@ -39,6 +44,9 @@ class ErrorHandler extends ChangeNotifier { deletedServerError = false; deletedServerSuccess = false; + changePasswordError = false; + explicitChangePasswordSuccess = false; + notifyListeners(); } @@ -58,6 +66,9 @@ class ErrorHandler extends ChangeNotifier { case deleteProfileErrorPrefix: handleDeleteProfileError(errorType); break; + case changePasswordErrorPrefix: + handleChangePasswordError(errorType); + break; case deletedServerErrorPrefix: handleDeletedServerError(errorType); } @@ -115,6 +126,21 @@ class ErrorHandler extends ChangeNotifier { } } + handleChangePasswordError(String errorType) { + // Reset add contact errors + changePasswordError = false; + explicitChangePasswordSuccess = false; + + switch (errorType) { + case successErrorType: + explicitChangePasswordSuccess = true; + break; + default: + changePasswordError = true; + break; + } + } + handleDeletedServerError(String errorType) { // reset deletedServerError = false; diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 3809c10d..feac77a3 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,10 @@ { "@@locale": "de", - "@@last_modified": "2021-12-17T23:48:01+01:00", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", "loadingCwtch": "Loading Cwtch...", "themeColorLabel": "Color Theme", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 467c586b..71c05d12 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,10 @@ { "@@locale": "en", - "@@last_modified": "2021-12-17T23:48:01+01:00", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", "loadingCwtch": "Loading Cwtch...", "themeColorLabel": "Color Theme", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 6a055fbb..c865430d 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,10 @@ { "@@locale": "es", - "@@last_modified": "2021-12-17T23:48:01+01:00", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", "loadingCwtch": "Loading Cwtch...", "themeColorLabel": "Color Theme", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 3d928173..6891a05a 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,8 +1,12 @@ { "@@locale": "fr", - "@@last_modified": "2021-12-17T23:48:01+01:00", - "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", - "loadingCwtch": "Loading Cwtch...", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", + "storageMigrationModalMessage": "Migration des profils vers un nouveau format de stockage. Cela peut prendre quelques minutes...", + "loadingCwtch": "Chargement de Cwtch...", "experimentClickableLinksDescription": "L'expérience des liens cliquables vous permet de cliquer sur les URLs partagés dans les messages", "themeNameWitch": "Sorcière", "themeNameVampire": "Vampire", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 621ece9b..60eece90 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,10 @@ { "@@locale": "it", - "@@last_modified": "2021-12-17T23:48:01+01:00", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", "loadingCwtch": "Loading Cwtch...", "themeColorLabel": "Color Theme", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 6bed7cd6..3e6146d7 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,10 @@ { "@@locale": "pl", - "@@last_modified": "2021-12-17T23:48:01+01:00", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", "loadingCwtch": "Loading Cwtch...", "themeColorLabel": "Color Theme", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index b77121d2..ac4af6f9 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,10 @@ { "@@locale": "pt", - "@@last_modified": "2021-12-17T23:48:01+01:00", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", "loadingCwtch": "Loading Cwtch...", "themeColorLabel": "Color Theme", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 6c1fb55a..b6e71cdf 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,10 @@ { "@@locale": "ru", - "@@last_modified": "2021-12-17T23:48:01+01:00", + "@@last_modified": "2021-12-19T02:59:05+01:00", + "msgAddToAccept": "Add this account to your contacts in order to accept this file.", + "btnSendFile": "Send File", + "msgConfirmSend": "Are you sure you want to send", + "msgFileTooBig": "File size cannot exceed 10 GB", "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", "loadingCwtch": "Loading Cwtch...", "themeColorLabel": "Тема", diff --git a/lib/model.dart b/lib/model.dart index 24bd1b1c..0b68d15a 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -414,12 +414,9 @@ class ProfileInfoState extends ChangeNotifier { void downloadUpdate(String fileKey, int progress, int numChunks) { if (!downloadActive(fileKey)) { + this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); if (progress < 0) { - this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); this._downloads[fileKey]!.interrupted = true; - notifyListeners(); - } else { - print("error: received progress for unknown download " + fileKey); } } else { if (this._downloads[fileKey]!.interrupted) { @@ -427,17 +424,16 @@ class ProfileInfoState extends ChangeNotifier { } this._downloads[fileKey]!.chunksDownloaded = progress; this._downloads[fileKey]!.chunksTotal = numChunks; - notifyListeners(); } + notifyListeners(); } void downloadMarkManifest(String fileKey) { if (!downloadActive(fileKey)) { - print("error: received download completion notice for unknown download " + fileKey); - } else { - this._downloads[fileKey]!.gotManifest = true; - notifyListeners(); + this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); } + this._downloads[fileKey]!.gotManifest = true; + notifyListeners(); } void downloadMarkFinished(String fileKey, String finalPath) { @@ -728,8 +724,8 @@ class ContactInfoState extends ChangeNotifier { return ret; } - void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data) { - this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false), data)); + void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { + this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); this.totalMessages += 1; } diff --git a/lib/models/message.dart b/lib/models/message.dart index 4df9eace..dc4c5b97 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -58,11 +58,15 @@ Message compileOverlay(MessageMetadata metadata, String messageData) { } Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) { - var cache = Provider.of(context).contactList.getContact(conversationIdentifier)?.messageCache; - if (cache != null && cache.length > index) { - if (cache[index] != null) { - return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper)); + try { + var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache; + if (cache != null && cache.length > index) { + if (cache[index] != null) { + return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper)); + } } + } catch (e) { + // provider check failed...make an expensive call... } try { @@ -75,7 +79,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co } return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", {}, false, true); + var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", {}, false, true, false); try { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); // There are 2 conditions in which this error condition can be met: @@ -103,7 +107,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co var ackd = messageWrapper['Acknowledged']; var error = messageWrapper['Error'] != null; var signature = messageWrapper['Signature']; - metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error); + metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false); return compileOverlay(metadata, messageWrapper['Message']); } catch (e) { @@ -112,7 +116,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co } }); } catch (e) { - return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", {}, false, true))); + return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", {}, false, true, false))); } } @@ -128,6 +132,7 @@ class MessageMetadata extends ChangeNotifier { final dynamic _attributes; bool _ackd; bool _error; + final bool isAuto; final String? signature; @@ -145,5 +150,6 @@ class MessageMetadata extends ChangeNotifier { notifyListeners(); } - MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error); + MessageMetadata( + this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error, this.isAuto); } diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index 9254c6d2..83387ac9 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -19,7 +19,6 @@ class FileMessage extends Message { @override Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( - key: key, value: this.metadata, builder: (bcontext, child) { dynamic shareObj = jsonDecode(this.content); @@ -35,7 +34,7 @@ class FileMessage extends Message { return MessageRow(MalformedBubble()); } - return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: key); + return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize, isAuto: metadata.isAuto), key: key); }); } @@ -55,13 +54,16 @@ class FileMessage extends Message { if (!validHash(rootHash, nonce)) { return MessageRow(MalformedBubble()); } - return FileBubble( - nameSuggestion, - rootHash, - nonce, - fileSize, - interactive: false, - ); + return Container( + alignment: Alignment.center, + child: FileBubble( + nameSuggestion, + rootHash, + nonce, + fileSize, + isAuto: metadata.isAuto, + interactive: false, + )); }); } diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index 149ba5e0..2965abc9 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -19,7 +19,6 @@ class InviteMessage extends Message { @override Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( - key: key, value: this.metadata, builder: (bcontext, child) { String inviteTarget; @@ -37,7 +36,7 @@ class InviteMessage extends Message { inviteTarget = jsonObj['GroupID']; inviteNick = jsonObj['GroupName']; } else { - return MessageRow(MalformedBubble(), key: key); + return MessageRow(MalformedBubble()); } } return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: key); diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 5a69ca91..2eb54b28 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/malformedmessage.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/quotedmessage.dart'; import 'package:flutter/widgets.dart'; @@ -51,7 +52,7 @@ class QuotedMessage extends Message { dynamic message = jsonDecode(this.content); return Text(message["body"]); } catch (e) { - return MalformedMessage(this.metadata).getWidget(context, Key("malformed")); + return MalformedBubble(); } }); } @@ -67,7 +68,7 @@ class QuotedMessage extends Message { dynamic message = jsonDecode(this.content); if (message["body"] == null || message["quotedHash"] == null) { - return MalformedMessage(this.metadata).getWidget(context, key); + return MalformedBubble(); } var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]); @@ -94,14 +95,14 @@ class QuotedMessage extends Message { return MessageRow( QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { if (localIndex != null) { - return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); + return messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); } return MalformedMessage(this.metadata); })), key: key); }); } catch (e) { - return MalformedMessage(this.metadata).getWidget(context, key); + return MalformedBubble(); } } } diff --git a/lib/settings.dart b/lib/settings.dart index f961af89..e887223e 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -12,6 +12,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; const TapirGroupsExperiment = "tapir-groups-experiment"; const ServerManagementExperiment = "servers-experiment"; const FileSharingExperiment = "filesharing"; +const ImagePreviewsExperiment = "filesharing-images"; const ClickableLinksExperiment = "clickable-links"; enum DualpaneMode { @@ -36,6 +37,7 @@ class Settings extends ChangeNotifier { bool blockUnknownConnections = false; bool streamerMode = false; + String _downloadPath = ""; void setTheme(String themeId, String mode) { theme = getTheme(themeId, mode); @@ -81,6 +83,9 @@ class Settings extends ChangeNotifier { _uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]); _uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]); + // auto-download folder + _downloadPath = settings["DownloadPath"] ?? ""; + // Push the experimental settings to Consumers of Settings notifyListeners(); } @@ -213,6 +218,26 @@ class Settings extends ChangeNotifier { } } + // checks experiment settings and file extension for image previews + // (ignores file size; if the user manually accepts the file, assume it's okay to preview) + bool shouldPreview(String path) { + var lpath = path.toLowerCase(); + return isExperimentEnabled(ImagePreviewsExperiment) && ( + lpath.endsWith(".jpg") || + lpath.endsWith(".jpeg") || + lpath.endsWith(".png") || + lpath.endsWith(".gif") || + lpath.endsWith(".webp") || + lpath.endsWith(".bmp") + ); + } + + String get downloadPath => _downloadPath; + set downloadPath(String newval) { + _downloadPath = newval; + notifyListeners(); + } + /// Construct a default settings object. Settings(this.locale, this.theme); @@ -232,6 +257,7 @@ class Settings extends ChangeNotifier { "FirstTime": false, "UIColumnModePortrait": uiColumnModePortrait.toString(), "UIColumnModeLandscape": uiColumnModeLandscape.toString(), + "DownloadPath": _downloadPath, }; } } diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 95706b92..b2bddf57 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -1,6 +1,8 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:math'; +import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch/cwtch.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -278,7 +280,7 @@ class _AddEditProfileViewState extends State { // TODO Toast } - void _createPressed() { + void _createPressed() async { // 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). @@ -301,17 +303,32 @@ class _AddEditProfileViewState extends State { } 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... + var profile = Provider.of(context, listen: false).onion; Provider.of(context, listen: false).nickname = ctrlrNick.value.text; - Provider.of(context, listen: false).cwtch.SetProfileAttribute(Provider.of(context, listen: false).onion, "profile.name", ctrlrNick.value.text); - final updatePasswordEvent = { - "EventType": "ChangePassword", - "Data": {"Password": ctrlrOldPass.text, "NewPassword": ctrlrPass.text} - }; - final updatePasswordEventJson = jsonEncode(updatePasswordEvent); + Provider.of(context, listen: false).cwtch.SetProfileAttribute(profile, "profile.name", ctrlrNick.value.text); + Provider.of(context, listen: false).cwtch.ChangePassword(profile, ctrlrOldPass.text, ctrlrPass.text, ctrlrPass2.text); - Provider.of(context, listen: false).cwtch.SendProfileEvent(Provider.of(context, listen: false).onion, updatePasswordEventJson); - - Navigator.of(context).pop(); + EnvironmentConfig.debugLog("waiting for change password response"); + Future.delayed(const Duration(milliseconds: 500), () { + if (globalErrorHandler.changePasswordError) { + // TODO: This isn't ideal, but because onChange can be fired during this future check + // and because the context can change after being popped we have this kind of double assertion... + // There is probably a better pattern to handle this... + if (AppLocalizations.of(context) != null) { + final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.passwordChangeError)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + Navigator.pop(context); + return; + } + } + }).whenComplete(() { + if (globalErrorHandler.explicitChangePasswordSuccess) { + final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.newPassword)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + Navigator.pop(context); + return; // otherwise round and round we go... + } + }); } } } diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index e6887350..c2d9880c 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/servers.dart'; +import 'package:cwtch/widgets/folderpicker.dart'; import 'package:cwtch/themes/cwtch.dart'; import 'package:cwtch/themes/ghost.dart'; import 'package:cwtch/themes/mermaid.dart'; @@ -254,21 +255,54 @@ class _GlobalSettingsViewState extends State { inactiveTrackColor: settings.theme.defaultButtonDisabledColor, secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor), ), - SwitchListTile( - title: Text(AppLocalizations.of(context)!.enableExperimentClickableLinks, style: TextStyle(color: settings.current().mainTextColor)), - subtitle: Text(AppLocalizations.of(context)!.experimentClickableLinksDescription), - value: settings.isExperimentEnabled(ClickableLinksExperiment), - onChanged: (bool value) { - if (value) { - settings.enableExperiment(ClickableLinksExperiment); - } else { - settings.disableExperiment(ClickableLinksExperiment); - } - saveSettings(context); - }, - activeTrackColor: settings.theme.defaultButtonColor, - inactiveTrackColor: settings.theme.defaultButtonDisabledColor, - secondary: Icon(Icons.link, color: settings.current().mainTextColor), + Visibility( + visible: settings.isExperimentEnabled(FileSharingExperiment), + child: Column(children: [ + SwitchListTile( + title: Text(AppLocalizations.of(context)!.settingImagePreviews, style: TextStyle(color: settings.current().mainTextColor)), + subtitle: Text(AppLocalizations.of(context)!.settingImagePreviewsDescription), + value: settings.isExperimentEnabled(ImagePreviewsExperiment), + onChanged: (bool value) { + if (value) { + settings.enableExperiment(ImagePreviewsExperiment); + settings.downloadPath = Provider.of(context, listen: false).cwtch.defaultDownloadPath(); + } else { + settings.disableExperiment(ImagePreviewsExperiment); + } + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonActiveColor, + inactiveTrackColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor), + ), + Visibility( + visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid, + child: CwtchFolderPicker( + label: AppLocalizations.of(context)!.settingDownloadFolder, + initialValue: settings.downloadPath, + onSave: (newVal) { + settings.downloadPath = newVal; + saveSettings(context); + }, + ), + ), + SwitchListTile( + title: Text(AppLocalizations.of(context)!.enableExperimentClickableLinks, style: TextStyle(color: settings.current().mainTextColor)), + subtitle: Text(AppLocalizations.of(context)!.experimentClickableLinksDescription), + value: settings.isExperimentEnabled(ClickableLinksExperiment), + onChanged: (bool value) { + if (value) { + settings.enableExperiment(ClickableLinksExperiment); + } else { + settings.disableExperiment(ClickableLinksExperiment); + } + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonActiveColor, + inactiveTrackColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(Icons.link, color: settings.current().mainTextColor), + ), + ]), ), ], )), diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 8b803daf..6febb013 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -5,6 +5,7 @@ import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/quotedmessage.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; @@ -37,6 +38,7 @@ class _MessageViewState extends State { int selectedContact = -1; ItemPositionsListener scrollListener = ItemPositionsListener.create(); ItemScrollController scrollController = ItemScrollController(); + File? imagePreview; @override void initState() { @@ -85,7 +87,7 @@ class _MessageViewState extends State { appBarButtons.add(IconButton( icon: Icon(Icons.attach_file, size: 24), tooltip: AppLocalizations.of(context)!.tooltipSendFile, - onPressed: _showFilePicker, + onPressed: (){_showFilePicker(context);}, )); } appBarButtons.add(IconButton( @@ -355,7 +357,7 @@ class _MessageViewState extends State { return contact.onion != Provider.of(context).onion; }, onChanged: (newVal) { setState(() { - this.selectedContact = Provider.of(context).contactList.findContact(newVal)!.identifier; + this.selectedContact = Provider.of(context, listen: false).contactList.findContact(newVal)!.identifier; }); })), SizedBox( @@ -376,7 +378,8 @@ class _MessageViewState extends State { }); } - void _showFilePicker() async { + void _showFilePicker(BuildContext ctx) async { + imagePreview = null; FilePickerResult? result = await FilePicker.platform.pickFiles(); if (result != null) { File file = File(result.files.first.path); @@ -384,11 +387,73 @@ class _MessageViewState extends State { // a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25) if (file.lengthSync() <= 10737418240) { print("Sending " + file.path); - _sendFile(file.path); + _confirmFileSend(ctx, file.path); } else { - print("file size cannot exceed 10 gigabytes"); - //todo: toast error + final snackBar = SnackBar( + content: Text(AppLocalizations.of(context)!.msgFileTooBig), + duration: Duration(seconds: 4), + ); + ScaffoldMessenger.of(ctx).showSnackBar(snackBar); } } } + + void _confirmFileSend(BuildContext ctx, String path) async { + showModalBottomSheet( + context: ctx, + builder: (BuildContext bcontext) { + var showPreview = false; + if (Provider.of(context, listen: false).shouldPreview(path)) { + showPreview = true; + if (imagePreview == null) { + imagePreview = new File(path); + } + } + return Container( + height: 300, // 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: [ + Text(AppLocalizations.of(context)!.msgConfirmSend + " $path?"), + SizedBox( + height: 20, + ), + Visibility(visible: showPreview, child: showPreview ? Image.file( + imagePreview!, + cacheHeight: 150, // limit the amount of space the image can decode too, we keep this high-ish to allow quality previews... + filterQuality: FilterQuality.medium, + fit: BoxFit.fill, + alignment: Alignment.center, + height: 150, + isAntiAlias: false, + errorBuilder: (context, error, stackTrace) { + return MalformedBubble(); + }, + ) : Container()), + Visibility(visible: showPreview, child: SizedBox(height: 10,)), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + ElevatedButton( + child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel), + onPressed: () { + Navigator.pop(bcontext); + }, + ), + SizedBox(width: 20,), + ElevatedButton( + child: Text(AppLocalizations.of(context)!.btnSendFile, semanticsLabel: AppLocalizations.of(context)!.btnSendFile), + onPressed: () { + _sendFile(path); + Navigator.pop(bcontext); + }, + ), + ]), + ], + )), + )); + }); + } } diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index fd49e9fe..6457ae6a 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cwtch/config.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:file_picker_desktop/file_picker_desktop.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -22,8 +23,9 @@ class FileBubble extends StatefulWidget { final String nonce; final int fileSize; final bool interactive; + final bool isAuto; - FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.interactive = true}); + FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true}); @override FileBubbleState createState() => FileBubbleState(); @@ -34,6 +36,8 @@ class FileBubble extends StatefulWidget { } class FileBubbleState extends State { + File? myFile; + @override void initState() { super.initState(); @@ -46,99 +50,148 @@ class FileBubbleState extends State { var borderRadiousEh = 15.0; var showFileSharing = Provider.of(context).isExperimentEnabled(FileSharingExperiment); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); + var downloadComplete = Provider.of(context).downloadComplete(widget.fileKey()); + var downloadInterrupted = Provider.of(context).downloadInterrupted(widget.fileKey()); + + if (flagStarted && !downloadInterrupted) { + Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, widget.fileKey()); + } + + var path = Provider.of(context).downloadFinalPath(widget.fileKey()); + if (downloadComplete) { + var lpath = path!.toLowerCase(); + if (lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp")) { + if (myFile == null) { + setState(() { + myFile = new File(path); + }); + } + } + } + + var downloadActive = Provider.of(context).downloadActive(widget.fileKey()); + var downloadGotManifest = Provider.of(context).downloadGotManifest(widget.fileKey()); // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; + var senderIsContact = false; if (!fromMe) { ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; + senderIsContact = true; } else { senderDisplayStr = Provider.of(context).senderHandle; } } + return LayoutBuilder(builder: (bcontext, constraints) { + var wdgSender = Visibility( + visible: widget.interactive, + child: SelectableText(senderDisplayStr + '\u202F', + style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor : Provider.of(context).theme.messageFromOtherTextColor))); + var isPreview = false; + var wdgMessage = !showFileSharing + ? Text(AppLocalizations.of(context)!.messageEnableFileSharing) + : fromMe + ? senderFileChrome(AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) + : (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, + Provider.of(context).downloadSpeed(widget.fileKey()))); + Widget wdgDecorations; - var wdgSender = Center( - widthFactor: 1, - child: SelectableText(senderDisplayStr + '\u202F', - style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor : Provider.of(context).theme.messageFromOtherTextColor))); - - var wdgMessage = !showFileSharing - ? Text(AppLocalizations.of(context)!.messageEnableFileSharing) - : fromMe - ? senderFileChrome(AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) - : (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, - Provider.of(context).downloadSpeed(widget.fileKey()))); - Widget wdgDecorations; - if (!showFileSharing) { - wdgDecorations = Text('\u202F'); - } else if (fromMe) { - wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); - } else if (Provider.of(context).downloadComplete(widget.fileKey())) { - // in this case, whatever marked download.complete would have also set the path - var path = Provider.of(context).downloadFinalPath(widget.fileKey())!; - wdgDecorations = Text(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F'); - } else if (Provider.of(context).downloadActive(widget.fileKey())) { - if (!Provider.of(context).downloadGotManifest(widget.fileKey())) { - wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); + if (!showFileSharing) { + wdgDecorations = Text('\u202F'); + } else if (fromMe) { + wdgDecorations = Visibility( + visible: widget.interactive, + child: MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate)); + } else if (downloadComplete) { + // in this case, whatever marked download.complete would have also set the path + if (Provider.of(context).shouldPreview(path!)) { + isPreview = true; + wdgDecorations = Center( + child: GestureDetector( + child: Padding( + padding: EdgeInsets.all(1.0), + child: Image.file( + myFile!, + cacheWidth: 2048, // limit the amount of space the image can decode too, we keep this high-ish to allow quality previews... + filterQuality: FilterQuality.medium, + fit: BoxFit.scaleDown, + alignment: Alignment.center, + height: MediaQuery.of(bcontext).size.height * 0.30, + isAntiAlias: false, + errorBuilder: (context, error, stackTrace) { + return MalformedBubble(); + }, + )), + onTap: () { + pop(bcontext, myFile!, wdgMessage); + }, + )); + } else { + wdgDecorations = Visibility(visible: widget.interactive, child: Text(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F')); + } + } else if (downloadActive) { + if (!downloadGotManifest) { + wdgDecorations = Visibility(visible: widget.interactive, child: Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F')); + } else { + wdgDecorations = Visibility( + visible: widget.interactive, + child: LinearProgressIndicator( + value: Provider.of(context).downloadProgress(widget.fileKey()), + color: Provider.of(context).theme.defaultButtonActiveColor, + )); + } + } else if (flagStarted) { + // in this case, the download was done in a previous application launch, + // so we probably have to request an info lookup + if (!downloadInterrupted) { + wdgDecorations = Text( + AppLocalizations.of(context)!.fileCheckingStatus + '...' + + '\u202F'); + } else { + var path = Provider.of(context).downloadFinalPath(widget.fileKey()) ?? ""; + wdgDecorations = Visibility( + visible: widget.interactive, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'), + ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton)) + ])); + } + } else if (!senderIsContact) { + wdgDecorations = Text(AppLocalizations.of(context)!.msgAddToAccept); + } else if (!widget.isAuto) { + wdgDecorations = Visibility( + visible: widget.interactive, + child: Center( + widthFactor: 1, + child: Wrap(children: [ + Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.downloadFileButton + '\u202F'), onPressed: _btnAccept)), + ]))); } else { - wdgDecorations = LinearProgressIndicator( - value: Provider.of(context).downloadProgress(widget.fileKey()), - color: Provider.of(context).theme.defaultButtonActiveColor, - ); + wdgDecorations = Container(); } - } else if (flagStarted) { - // in this case, the download was done in a previous application launch, - // so we probably have to request an info lookup - if (!Provider.of(context).downloadInterrupted(widget.fileKey())) { - wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F'); - Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, widget.fileKey()); - } else { - var path = Provider.of(context).downloadFinalPath(widget.fileKey()) ?? ""; - wdgDecorations = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'), - ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton)) - ]); - } - } else { - wdgDecorations = Center( - widthFactor: 1, - child: Wrap(children: [ - Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.downloadFileButton + '\u202F'), onPressed: _btnAccept)), - ])); - } - return LayoutBuilder(builder: (context, constraints) { - //print(constraints.toString()+", "+constraints.maxWidth.toString()); - return Center( - widthFactor: 1.0, - child: Container( - decoration: BoxDecoration( - color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor : Provider.of(context).theme.messageFromOtherBackgroundColor, - border: Border.all(color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor : Provider.of(context).theme.messageFromOtherBackgroundColor, width: 1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(borderRadiousEh), - topRight: Radius.circular(borderRadiousEh), - bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, - bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), - ), - ), - child: Center( - widthFactor: 1.0, - child: Padding( - padding: EdgeInsets.all(9.0), - child: Wrap(alignment: WrapAlignment.start, children: [ - Center( - widthFactor: 1.0, - child: Column( - crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: fromMe - ? [wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)] - : [wdgSender, wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)]), - ) - ]))))); + return Container( + constraints: constraints, + decoration: BoxDecoration( + color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor : Provider.of(context).theme.messageFromOtherBackgroundColor, + border: Border.all(color: fromMe ? Provider.of(context).theme.messageFromMeBackgroundColor : Provider.of(context).theme.messageFromOtherBackgroundColor, width: 1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(borderRadiousEh), + topRight: Radius.circular(borderRadiousEh), + bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, + bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), + ), + ), + child: Padding( + padding: EdgeInsets.all(9.0), + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)] : [wdgSender, isPreview ? Container() : wdgMessage, wdgDecorations]), + )); }); } @@ -169,7 +222,7 @@ class FileBubbleState extends State { Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); Provider.of(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true"); //Provider.of(context, listen: false).flags |= 0x02; - ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); + ContactInfoState? contact = Provider.of(context, listen: false).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey()); } @@ -294,4 +347,32 @@ class FileBubbleState extends State { )), ); } + + void pop(context, File myFile, Widget meta) async { + await showDialog( + context: context, + builder: (_) => Dialog( + alignment: Alignment.center, + child: Container( + padding: EdgeInsets.all(10), + child: Column(children: [ + meta, + Image.file( + myFile, + cacheWidth: (MediaQuery.of(context).size.width * 0.6).floor(), + width: (MediaQuery.of(context).size.width * 0.6), + height: (MediaQuery.of(context).size.height * 0.6), + fit: BoxFit.scaleDown, + ), + Visibility(visible: !Platform.isAndroid, child: Text(myFile.path, textAlign: TextAlign.center)), + Visibility(visible: Platform.isAndroid, child: IconButton(icon: Icon(Icons.arrow_downward), onPressed: androidExport)), + ]), + ))); + } + + void androidExport() async { + if (myFile != null) { + Provider.of(context, listen: false).cwtch.ExportPreviewedFile(myFile!.path, widget.nameSuggestion); + } + } } diff --git a/lib/widgets/folderpicker.dart b/lib/widgets/folderpicker.dart new file mode 100644 index 00000000..7e9f9bfd --- /dev/null +++ b/lib/widgets/folderpicker.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'dart:io'; + +import 'package:file_picker_desktop/file_picker_desktop.dart'; +import 'buttontextfield.dart'; +import 'cwtchlabel.dart'; + +class CwtchFolderPicker extends StatefulWidget { + final String label; + final String initialValue; + final Function(String)? onSave; + const CwtchFolderPicker({Key? key, this.label = "", this.initialValue = "", this.onSave}) : super(key: key); + + @override + _CwtchFolderPickerState createState() => _CwtchFolderPickerState(); +} + +class _CwtchFolderPickerState extends State { + final TextEditingController ctrlrVal = TextEditingController(); + + @override + void initState() { + super.initState(); + ctrlrVal.text = widget.initialValue; + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.all(10), + padding: EdgeInsets.all(2), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + CwtchLabel(label: widget.label), + SizedBox( + height: 20, + ), + CwtchButtonTextField( + controller: ctrlrVal, + readonly: Platform.isAndroid, + onPressed: () async { + if (Platform.isAndroid) { + return; + } + + try { + var selectedDirectory = await getDirectoryPath(); + if (selectedDirectory != null) { + //File directory = File(selectedDirectory); + selectedDirectory += "/"; + ctrlrVal.text = selectedDirectory; + if (widget.onSave != null) { + widget.onSave!(selectedDirectory); + } + } else { + // User canceled the picker + } + } catch (e) { + print(e); + } + }, + icon: Icon(Icons.folder), + tooltip: "Browse", //todo: l18n + ) + ])); + } +} diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart index a5d9e418..3c4c1a55 100644 --- a/lib/widgets/profilerow.dart +++ b/lib/widgets/profilerow.dart @@ -102,7 +102,7 @@ class _ProfileRowState extends State { } void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) { - Provider.of(context).reset(); + Provider.of(context, listen: false).reset(); Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return MultiProvider( diff --git a/pubspec.lock b/pubspec.lock index e990cfd9..3c1db18c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,7 +417,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.7" + version: "0.4.3" typed_data: dependency: transitive description: