From a99a00de309f2ca9d1ca4f4ad9f08a9a0bdf0766 Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 28 May 2021 15:56:45 -0700 Subject: [PATCH 01/17] WIP: partial android service migration --- android/app/build.gradle | 29 +++++ android/app/src/main/AndroidManifest.xml | 2 + .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 122 ++++++++++++++++++ .../kotlin/im/cwtch/flwtch/MainActivity.kt | 37 +++--- pubspec.lock | 4 +- 5 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index 4b49db2..2786507 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -69,6 +69,15 @@ android { signingConfig signingConfigs.release } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } flutter { @@ -82,4 +91,24 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2" implementation "com.airbnb.android:lottie:3.5.0" implementation "com.android.support.constraint:constraint-layout:2.0.4" + + // WorkManager + + // (Java only) + //implementation("androidx.work:work-runtime:$work_version") + + // Kotlin + coroutines + implementation("androidx.work:work-runtime-ktx:2.5.0") + + // optional - RxJava2 support + //implementation("androidx.work:work-rxjava2:$work_version") + + // optional - GCMNetworkManager support + //implementation("androidx.work:work-gcm:$work_version") + + // optional - Test helpers + //androidTestImplementation("androidx.work:work-testing:$work_version") + + // optional - Multiprocess support + implementation "androidx.work:work-multiprocess:2.5.0" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 43ed283..1162d40 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -42,4 +42,6 @@ + + diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt new file mode 100644 index 0000000..b14af9f --- /dev/null +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -0,0 +1,122 @@ +package im.cwtch.flwtch + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.graphics.Color +import android.os.Build +import android.util.Log + +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import androidx.work.WorkManager + +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +import org.json.JSONObject + +import cwtch.Cwtch + +class FlwtchWorker(context: Context, parameters: WorkerParameters) : + CoroutineWorker(context, parameters) { + private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus" + + private val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as + NotificationManager + + override suspend fun doWork(): Result { + val appDir = inputData.getString(KEY_APP_DIR) + ?: return Result.failure() + val torPath = inputData.getString(KEY_TOR_PATH) + ?: return Result.failure() + // Mark the Worker as important + val progress = "Trying to do a Flwtch" + setForeground(createForegroundInfo(progress)) + download(appDir, torPath) + return Result.success() + } + + private suspend fun download(appDir: String, torPath: String) { + Cwtch.startCwtch(appDir, torPath) + // seperate coroutine to poll event bus and send to dart + //Log.i("FlwtchWorker.kt", "got event chan: " + eventbus_chan + " launching corouting...") + GlobalScope.launch(Dispatchers.IO) { + while(true) { + val evt = AppbusEvent(Cwtch.getAppBusEvent()) + Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) + launch(Dispatchers.Main) { + //todo: this elides evt.EventID which may be needed at some point? + val flutterEngine = FlutterEngine(applicationContext) + val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) + eventbus_chan.invokeMethod(evt.EventType, evt.Data) + } + } + } + } + + // Creates an instance of ForegroundInfo which can be used to update the + // ongoing notification. + private fun createForegroundInfo(progress: String): ForegroundInfo { + val id = "flwtch" + val title = "Flwtch" + val cancel = "Nevermind"//todo: translate + // This PendingIntent can be used to cancel the worker + val intent = WorkManager.getInstance(applicationContext) + .createCancelPendingIntent(getId()) + + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(id, id) + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + val notification = NotificationCompat.Builder(applicationContext, id) + .setContentTitle(title) + .setTicker(title) + .setContentText(progress) + .setSmallIcon(R.mipmap.knott) + .setOngoing(true) + // Add the cancel action to the notification which can + // be used to cancel the worker + .addAction(android.R.drawable.ic_delete, cancel, intent) + .build() + + return ForegroundInfo(101, notification) + } + + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_NONE) + chan.lightColor = Color.BLUE + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + notificationManager.createNotificationChannel(chan) + return channelId + } + + companion object { + const val KEY_APP_DIR = "KEY_APP_DIR" + const val KEY_TOR_PATH = "KEY_TOR_PATH" + } + + class AppbusEvent(json: String) : JSONObject(json) { + val EventType = this.optString("EventType") + val EventID = this.optString("EventID") + val Data = this.optString("Data") + } +} 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 c82a32e..dbac7d8 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -7,6 +7,11 @@ import android.os.Bundle import android.os.Looper import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkRequest + import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -44,8 +49,6 @@ class MainActivity: FlutterActivity() { // Note: this methods are invoked on the main thread. MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) } - - } private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) { @@ -70,21 +73,25 @@ class MainActivity: FlutterActivity() { val appDir = (call.argument("appDir") as? String) ?: ""; val torPath = (call.argument("torPath") as? String) ?: "tor"; Log.i("MainActivity.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") - Cwtch.startCwtch(appDir, torPath) + + //Cwtch.startCwtch(appDir, torPath)// todo + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_APP_DIR, appDir).putString(FlwtchWorker.KEY_TOR_PATH, torPath).build() + val uploadWorkRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).build() + WorkManager.getInstance(this).enqueue(uploadWorkRequest) // seperate coroutine to poll event bus and send to dart - val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) - Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...") - GlobalScope.launch(Dispatchers.IO) { - while(true) { - val evt = AppbusEvent(Cwtch.getAppBusEvent()) - Log.i("MainActivity.kt", "got appbusEvent: " + evt) - launch(Dispatchers.Main) { - //todo: this elides evt.EventID which may be needed at some point? - eventbus_chan.invokeMethod(evt.EventType, evt.Data) - } - } - } +// val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) +// Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...") +// GlobalScope.launch(Dispatchers.IO) { +// while(true) { +// val evt = AppbusEvent(Cwtch.getAppBusEvent()) +// Log.i("MainActivity.kt", "got appbusEvent: " + evt) +// launch(Dispatchers.Main) { +// //todo: this elides evt.EventID which may be needed at some point? +// eventbus_chan.invokeMethod(evt.EventType, evt.Data) +// } +// } +// } } "SelectProfile" -> { val onion = (call.argument("profile") as? String) ?: ""; diff --git a/pubspec.lock b/pubspec.lock index 8c01490..011cd94 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.5.0" boolean_selector: dependency: transitive description: @@ -371,7 +371,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "6.2.0" + version: "6.1.0+1" webdriver: dependency: transitive description: From 599993d2d2892549a155b1ae383702a103dc1298 Mon Sep 17 00:00:00 2001 From: erinn Date: Tue, 1 Jun 2021 14:01:47 -0700 Subject: [PATCH 02/17] WIP: partial android service migration --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 152 ++++++++++++++- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 174 ++++-------------- android/build.gradle | 6 +- pubspec.lock | 2 +- 4 files changed, 184 insertions(+), 150 deletions(-) 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 b14af9f..c7e5ef3 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -36,18 +36,156 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : NotificationManager override suspend fun doWork(): Result { - val appDir = inputData.getString(KEY_APP_DIR) + val method = inputData.getString(KEY_METHOD) ?: return Result.failure() - val torPath = inputData.getString(KEY_TOR_PATH) + val args = inputData.getString(KEY_ARGS) ?: return Result.failure() + Log.i("FlwtchWorker.kt", "got call $method with args $args") // Mark the Worker as important val progress = "Trying to do a Flwtch" setForeground(createForegroundInfo(progress)) - download(appDir, torPath) + handleCwtch(method, args) return Result.success() } - private suspend fun download(appDir: String, torPath: String) { + private suspend fun handleCwtch(method: String, args: String) { + var a = JSONObject(args); + when (method) { + "Start" -> { + Log.i("FlwtchWorker.kt", "handleAppInfo Start") + val appDir = (a.get("appDir") as? String) ?: ""; + val torPath = (a.get("torPath") as? String) ?: "tor"; + Log.i("FlwtchWorker.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") + + Cwtch.startCwtch(appDir, torPath) + + // infinite coroutine :) + while(true) { + val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent()) + Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) + if (isStopped) { + Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") + } + //todo: this elides evt.EventID which may be needed at some point? + val data = Data.Builder().putString("EventType", evt.EventType).putString("Data", evt.Data).build() + setProgress(data) + Thread.sleep(200) + } + } + "SelectProfile" -> { + val onion = (a.get("profile") as? String) ?: ""; + Cwtch.selectProfile(onion) + } + "CreateProfile" -> { + val nick = (a.get("nick") as? String) ?: ""; + val pass = (a.get("pass") as? String) ?: ""; + Cwtch.createProfile(nick, pass) + } + "LoadProfiles" -> { + val pass = (a.get("pass") as? String) ?: ""; + Cwtch.loadProfiles(pass) + } + "GetProfiles" -> Result.success(Data.Builder().putString("result", Cwtch.getProfiles()).build()) + // "ACNEvents" -> result.success(Cwtch.acnEvents()) + "ContactEvents" -> Result.success(Data.Builder().putString("result", Cwtch.contactEvents()).build()) + "NumMessages" -> { + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) + } + "GetMessage" -> { + //Log.i("MainActivivity.kt", (a.get("index"))); + +// var args : HashMap = a.gets(); +// Log.i("FlwtchWorker.kt", args); + + + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + val indexI = a.getInt("index") ?: 0; + Log.i("FlwtchWorker.kt", "indexI = " + indexI) + Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) + } + "GetMessages" -> { + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + val start = (a.get("start") as? Long) ?: 0; + val end = (a.get("end") as? Long) ?: 0; + Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) + } + "AcceptContact" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + Cwtch.acceptContact(profile, handle); + } + "BlockContact" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + Cwtch.blockContact(profile, handle); + } + "DebugResetContact" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + Cwtch.debugResetContact(profile, handle); + } + "SendMessage" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + val message = (a.get("message") as? String) ?: ""; + Cwtch.sendMessage(profile, handle, message); + } + "SendInvitation" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val handle = (a.get("handle") as? String) ?: ""; + val target = (a.get("target") as? String) ?: ""; + Cwtch.sendInvitation(profile, handle, target); + } + "SendProfileEvent" -> { + val onion = (a.get("onion") as? String) ?: ""; + val jsonEvent = (a.get("jsonEvent") as? String) ?: ""; + Cwtch.sendProfileEvent(onion, jsonEvent); + } + "SendAppEvent" -> { + val jsonEvent = (a.get("jsonEvent") as? String) ?: ""; + Cwtch.sendAppEvent(jsonEvent); + } + "ResetTor" -> { + Cwtch.resetTor(); + } + "ImportBundle" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val bundle = (a.get("bundle") as? String) ?: ""; + Cwtch.importBundle(profile, bundle); + } + "SetGroupAttribute" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val groupHandle = (a.get("groupHandle") as? String) ?: ""; + val key = (a.get("key") as? String) ?: ""; + val value = (a.get("value") as? String) ?: ""; + Cwtch.setGroupAttribute(profile, groupHandle, key, value); + } + "CreateGroup" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val server = (a.get("server") as? String) ?: ""; + val groupName = (a.get("groupname") as? String) ?: ""; + Cwtch.createGroup(profile, server, groupName); + } + "LeaveGroup" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val groupHandle = (a.get("groupHandle") as? String) ?: ""; + Log.i("FlwtchWorker.kt", "LeaveGroup: need to recompile aar and uncomment this line")//todo + //Cwtch.leaveGroup(profile, groupHandle); + } + "RejectInvite" -> { + val profile = (a.get("ProfileOnion") as? String) ?: ""; + val groupHandle = (a.get("groupHandle") as? String) ?: ""; + Cwtch.rejectInvite(profile, groupHandle); + } + else -> Result.failure() + } + } + + private suspend fun launchCwtch(appDir: String, torPath: String) { Cwtch.startCwtch(appDir, torPath) // seperate coroutine to poll event bus and send to dart //Log.i("FlwtchWorker.kt", "got event chan: " + eventbus_chan + " launching corouting...") @@ -103,15 +241,15 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : private fun createNotificationChannel(channelId: String, channelName: String): String{ val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE) - chan.lightColor = Color.BLUE + chan.lightColor = Color.MAGENTA chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE notificationManager.createNotificationChannel(chan) return channelId } companion object { - const val KEY_APP_DIR = "KEY_APP_DIR" - const val KEY_TOR_PATH = "KEY_TOR_PATH" + const val KEY_METHOD = "KEY_METHOD" + const val KEY_ARGS = "KEY_ARGS" } class AppbusEvent(json: String) : JSONObject(json) { 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 e1aa843..e2dccaf 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -6,11 +6,8 @@ import android.content.pm.PackageManager import android.os.Bundle import android.os.Looper import android.util.Log - -import androidx.work.Data -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkRequest +import androidx.lifecycle.Observer +import androidx.work.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -67,141 +64,40 @@ class MainActivity: FlutterActivity() { } private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "Start" -> { - Log.i("MainActivity.kt", "handleAppInfo Start") - val appDir = (call.argument("appDir") as? String) ?: ""; - val torPath = (call.argument("torPath") as? String) ?: "tor"; - Log.i("MainActivity.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") + val argmap: Map = call.arguments as Map + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() + var tag = "" + if (call.method == "Start") { + tag = "cwtchEventBus" + } + val workRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).addTag(tag).build() + WorkManager.getInstance(this).enqueue(workRequest) + if (call.method == "Start") { + val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) + WorkManager.getInstance(applicationContext) + // requestId is the WorkRequest id + .getWorkInfosByTagLiveData("cwtchEventBus") + .observeForever(Observer> { listOfWorkInfo -> + if (listOfWorkInfo.isNullOrEmpty()) { + return@Observer + } - //Cwtch.startCwtch(appDir, torPath)// todo - val data: Data = Data.Builder().putString(FlwtchWorker.KEY_APP_DIR, appDir).putString(FlwtchWorker.KEY_TOR_PATH, torPath).build() - val uploadWorkRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).build() - WorkManager.getInstance(this).enqueue(uploadWorkRequest) - - // seperate coroutine to poll event bus and send to dart -// val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) -// Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...") -// GlobalScope.launch(Dispatchers.IO) { -// while(true) { -// val evt = AppbusEvent(Cwtch.getAppBusEvent()) -// Log.i("MainActivity.kt", "got appbusEvent: " + evt) -// launch(Dispatchers.Main) { -// //todo: this elides evt.EventID which may be needed at some point? -// eventbus_chan.invokeMethod(evt.EventType, evt.Data) -// } -// } -// } - } - "SelectProfile" -> { - val onion = (call.argument("profile") as? String) ?: ""; - Cwtch.selectProfile(onion) - } - "CreateProfile" -> { - val nick = (call.argument("nick") as? String) ?: ""; - val pass = (call.argument("pass") as? String) ?: ""; - Cwtch.createProfile(nick, pass) - } - "LoadProfiles" -> { - val pass = (call.argument("pass") as? String) ?: ""; - Cwtch.loadProfiles(pass) - } - "GetProfiles" -> result.success(Cwtch.getProfiles()) - // "ACNEvents" -> result.success(Cwtch.acnEvents()) - "ContactEvents" -> result.success(Cwtch.contactEvents()) - "NumMessages" -> { - val profile = (call.argument("profile") as? String) ?: ""; - val handle = (call.argument("contact") as? String) ?: ""; - result.success(Cwtch.numMessages(profile, handle)) - } - "GetMessage" -> { - //Log.i("MainActivivity.kt", (call.argument("index"))); - -// var args : HashMap = call.arguments(); -// Log.i("MainActivity.kt", args); - - - val profile = (call.argument("profile") as? String) ?: ""; - val handle = (call.argument("contact") as? String) ?: ""; - val indexI = call.argument("index") ?: 0; - Log.i("MainActivity.kt", "indexI = " + indexI) - result.success(Cwtch.getMessage(profile, handle, indexI.toLong())) - } - "GetMessages" -> { - val profile = (call.argument("profile") as? String) ?: ""; - val handle = (call.argument("contact") as? String) ?: ""; - val start = (call.argument("start") as? Long) ?: 0; - val end = (call.argument("end") as? Long) ?: 0; - result.success(Cwtch.getMessages(profile, handle, start, end)) - } - "AcceptContact" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - Cwtch.acceptContact(profile, handle); - } - "BlockContact" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - Cwtch.blockContact(profile, handle); - } - "DebugResetContact" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - Cwtch.debugResetContact(profile, handle); - } - "SendMessage" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - val message = (call.argument("message") as? String) ?: ""; - Cwtch.sendMessage(profile, handle, message); - } - "SendInvitation" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val handle = (call.argument("handle") as? String) ?: ""; - val target = (call.argument("target") as? String) ?: ""; - Cwtch.sendInvitation(profile, handle, target); - } - "SendProfileEvent" -> { - val onion = (call.argument("onion") as? String) ?: ""; - val jsonEvent = (call.argument("jsonEvent") as? String) ?: ""; - Cwtch.sendProfileEvent(onion, jsonEvent); - } - "SendAppEvent" -> { - val jsonEvent = (call.argument("jsonEvent") as? String) ?: ""; - Cwtch.sendAppEvent(jsonEvent); - } - "ResetTor" -> { - Cwtch.resetTor(); - } - "ImportBundle" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val bundle = (call.argument("bundle") as? String) ?: ""; - Cwtch.importBundle(profile, bundle); - } - "SetGroupAttribute" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val groupHandle = (call.argument("groupHandle") as? String) ?: ""; - val key = (call.argument("key") as? String) ?: ""; - val value = (call.argument("value") as? String) ?: ""; - Cwtch.setGroupAttribute(profile, groupHandle, key, value); - } - "CreateGroup" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val server = (call.argument("server") as? String) ?: ""; - val groupName = (call.argument("groupname") as? String) ?: ""; - Cwtch.createGroup(profile, server, groupName); - } - "LeaveGroup" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val groupHandle = (call.argument("groupHandle") as? String) ?: ""; - Cwtch.leaveGroup(profile, groupHandle); - } - "RejectInvite" -> { - val profile = (call.argument("ProfileOnion") as? String) ?: ""; - val groupHandle = (call.argument("groupHandle") as? String) ?: ""; - Cwtch.rejectInvite(profile, groupHandle); - } - else -> result.notImplemented() + for (workInfo in listOfWorkInfo) { + if (workInfo != null) { + val progress = workInfo.progress + val eventType = progress.getString("EventType") ?: "" + val eventData = progress.getString("Data") + Log.i("MainActivity.kt", "got event $progress $eventType $eventData") + try { + eventbus_chan.invokeMethod(eventType, eventData) + } catch (e: Exception) { + Log.i("MainActivity.kt", "event bus exception") + } + } else { + Log.i("MainActivity.kt", "got null workInfo") + } + } + }) } } diff --git a/android/build.gradle b/android/build.gradle index 361fb7a..5685ae0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,6 +27,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { - delete rootProject.buildDir -} +//task clean(type: Delete) { +// delete rootProject.buildDir +//} diff --git a/pubspec.lock b/pubspec.lock index c7f8be5..58bca12 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,7 +105,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "6.1.0" flutter: dependency: "direct main" description: flutter From 6972028ecf00fc1ed87b253186c301dab2e55791 Mon Sep 17 00:00:00 2001 From: erinn Date: Wed, 2 Jun 2021 13:21:23 -0700 Subject: [PATCH 03/17] WIP: partial android service migration --- android/app/build.gradle | 5 +++++ .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 18 +++++++----------- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 9 ++++++--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2786507..91220d1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -111,4 +111,9 @@ dependencies { // optional - Multiprocess support implementation "androidx.work:work-multiprocess:2.5.0" + + // end of workmanager deps + + // ipc + implementation "io.reactivex:rxkotlin:1.x.y" } 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 c7e5ef3..1ff7a92 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -7,25 +7,17 @@ import android.content.Context import android.graphics.Color import android.os.Build import android.util.Log - import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.ForegroundInfo -import androidx.work.WorkerParameters -import androidx.work.WorkManager - +import androidx.work.* +import cwtch.Cwtch import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel - import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch - import org.json.JSONObject -import cwtch.Cwtch class FlwtchWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { @@ -67,7 +59,11 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") } //todo: this elides evt.EventID which may be needed at some point? - val data = Data.Builder().putString("EventType", evt.EventType).putString("Data", evt.Data).build() + val data = Data.Builder() + .putString("EventType", evt.EventType) + .putString("Data", evt.Data) + .putString("EventID", evt.EventID) + .build() setProgress(data) Thread.sleep(200) } 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 e2dccaf..1c416c5 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -77,7 +77,7 @@ class MainActivity: FlutterActivity() { WorkManager.getInstance(applicationContext) // requestId is the WorkRequest id .getWorkInfosByTagLiveData("cwtchEventBus") - .observeForever(Observer> { listOfWorkInfo -> + .observe(this, Observer> { listOfWorkInfo -> if (listOfWorkInfo.isNullOrEmpty()) { return@Observer } @@ -87,9 +87,12 @@ class MainActivity: FlutterActivity() { val progress = workInfo.progress val eventType = progress.getString("EventType") ?: "" val eventData = progress.getString("Data") - Log.i("MainActivity.kt", "got event $progress $eventType $eventData") + val output = progress.keyValueMap.toString() try { - eventbus_chan.invokeMethod(eventType, eventData) + if (eventType != "") { + Log.i("MainActivity.kt", "got event $output $eventType $eventData") + eventbus_chan.invokeMethod(eventType, eventData) + } } catch (e: Exception) { Log.i("MainActivity.kt", "event bus exception") } From 332fad4108eff66050b82bde975cc15e87a70aa4 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Fri, 4 Jun 2021 22:38:58 -0700 Subject: [PATCH 04/17] android versioncode bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3bcc763..b232973 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+9 +version: 1.0.0+10 environment: sdk: ">=2.12.0 <3.0.0" From b0f4a085b8c960d964e310a7bab8646d0ee2303d Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 4 Jun 2021 17:03:53 -0700 Subject: [PATCH 05/17] Fix invitation wrapping --- lib/widgets/invitationbubble.dart | 98 +++++++++++++++---------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 3e5e8d4..600dad1 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -63,15 +63,15 @@ class InvitationBubbleState extends State { if (fromMe) { wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); } else if (isAccepted) { - wdgDecorations = Text("Accepted!"); + wdgDecorations = Text("Accepted!" + '\u202F'); } else if (this.rejected) { - wdgDecorations = Text("Rejected."); + wdgDecorations = Text("Rejected." + '\u202F'); } else { wdgDecorations = Center( widthFactor: 1, - child: Row(children: [ - Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Reject"), onPressed: _btnReject)), - Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Accept"), onPressed: _btnAccept)), + child: Wrap(children: [ + Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Reject" + '\u202F'), onPressed: _btnReject)), + Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Accept" + '\u202F'), onPressed: _btnAccept)), ])); } @@ -95,8 +95,8 @@ class InvitationBubbleState extends State { widthFactor: 1.0, child: Padding( padding: EdgeInsets.all(9.0), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(4), child: Icon(CwtchIcons.send_invite, size: 32))), + child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ + Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(CwtchIcons.send_invite, size: 32))), Center( widthFactor: 1.0, child: Column( @@ -129,53 +129,49 @@ class InvitationBubbleState extends State { // Construct an invite chrome for the sender Widget senderInviteChrome(String chrome, String targetName, String myKey) { - return Center( - widthFactor: 1, - child: Row(children: [ - SelectableText( - chrome, - focusNode: _focus, - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ), - SelectableText( - targetName + '\u202F', - key: Key(myKey), - focusNode: _focus, - style: TextStyle( - color: Provider.of(context).theme.messageFromMeTextColor(), - ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ) - ])); + return Wrap(children: [ + SelectableText( + chrome + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ), + SelectableText( + targetName + '\u202F', + key: Key(myKey), + style: TextStyle( + color: Provider.of(context).theme.messageFromMeTextColor(), + ), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ) + ]); } // Construct an invite chrome Widget inviteChrome(String chrome, String targetName, String targetId, String myKey) { - return Center( - widthFactor: 1, - child: Row(children: [ - SelectableText( - chrome, - focusNode: _focus, - style: TextStyle( - color: Provider.of(context).theme.messageFromOtherTextColor(), - ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ), - SelectableText( - targetName, - key: Key(myKey), - focusNode: _focus, - style: TextStyle(color: Provider.of(context).theme.messageFromOtherTextColor()), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ) - ])); + return Wrap(children: [ + SelectableText( + chrome + '\u202F', + style: TextStyle( + color: Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + maxLines: 2, + ), + SelectableText( + targetName + '\u202F', + key: Key(myKey), + style: TextStyle(color: Provider.of(context).theme.messageFromOtherTextColor()), + textAlign: TextAlign.left, + maxLines: 2, + textWidthBasis: TextWidthBasis.longestLine, + ) + ]); } } From a4c7fe6ebff9d34e47ec0d2c2f6d4baaf0f88c4f Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Jun 2021 14:14:05 -0700 Subject: [PATCH 06/17] Fix Self-Invitations --- lib/views/messageview.dart | 4 +++- lib/widgets/DropdownContacts.dart | 8 +++++++- lib/widgets/invitationbubble.dart | 26 ++++++++++++++++++-------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index ff465be..b3d4488 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -177,7 +177,9 @@ class _MessageViewState extends State { ), ChangeNotifierProvider.value( value: Provider.of(ctx, listen: false), - child: DropdownContacts(onChanged: (newVal) { + child: DropdownContacts(filter: (contact) { + return contact.onion != Provider.of(context).onion; + }, onChanged: (newVal) { setState(() { this.selectedContact = newVal; }); diff --git a/lib/widgets/DropdownContacts.dart b/lib/widgets/DropdownContacts.dart index 7cb3dcc..aed9c4a 100644 --- a/lib/widgets/DropdownContacts.dart +++ b/lib/widgets/DropdownContacts.dart @@ -3,6 +3,10 @@ import 'package:provider/provider.dart'; import '../model.dart'; +bool noFilter(ContactInfoState peer) { + return true; +} + // Dropdown menu populated from Provider.of's contact list // Includes both peers and groups; begins empty/nothing selected // Displays nicknames to UI but uses handles as values @@ -10,8 +14,10 @@ import '../model.dart'; class DropdownContacts extends StatefulWidget { DropdownContacts({ required this.onChanged, + this.filter = noFilter, }); final Function(dynamic) onChanged; + final bool Function(ContactInfoState) filter; @override _DropdownContactsState createState() => _DropdownContactsState(); @@ -24,7 +30,7 @@ class _DropdownContactsState extends State { Widget build(BuildContext context) { return DropdownButton( value: this.selected, - items: Provider.of(context, listen: false).contactList.contacts.map>((ContactInfoState contact) { + items: Provider.of(context, listen: false).contactList.contacts.where(widget.filter).map>((ContactInfoState contact) { return DropdownMenuItem(value: contact.onion, child: Text(contact.nickname)); }).toList(), onChanged: (String? newVal) { diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 600dad1..10d0569 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../main.dart'; @@ -54,10 +55,18 @@ class InvitationBubbleState extends State { style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor()))); // todo: translations + + // If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from + // some kind of malfeasance. + var selfInvite = Provider.of(context).inviteNick == Provider.of(context).onion; + if (selfInvite) { + return MalformedBubble(); + } + var wdgMessage = fromMe ? senderInviteChrome("You sent an invitation for", isGroup ? "a group" : Provider.of(context).message, myKey) - : inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : "This is a contact suggestion for:", Provider.of(context).inviteNick, - Provider.of(context).inviteTarget, myKey); + : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : "This is a contact suggestion for:", Provider.of(context).inviteNick, + Provider.of(context).inviteTarget, myKey)); Widget wdgDecorations; if (fromMe) { @@ -98,12 +107,13 @@ class InvitationBubbleState extends State { child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(CwtchIcons.send_invite, size: 32))), Center( - widthFactor: 1.0, - child: Column( - crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])), + widthFactor: 1.0, + child: Column( + crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]), + ) ]))))); }); } From aa0a1d9a262270e6a2a1acbb4b0d798d988c6dea Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Jun 2021 15:12:24 -0700 Subject: [PATCH 07/17] Flutter Upgrade + Translations --- lib/l10n/intl_de.arb | 4 ++++ lib/l10n/intl_en.arb | 4 ++++ lib/l10n/intl_es.arb | 4 ++++ lib/l10n/intl_fr.arb | 4 ++++ lib/l10n/intl_it.arb | 4 ++++ lib/l10n/intl_pt.arb | 4 ++++ lib/opaque.dart | 2 ++ lib/widgets/invitationbubble.dart | 13 +++++++------ lib/widgets/messagelist.dart | 3 +-- lib/widgets/messagerow.dart | 5 ++++- linux/flutter/generated_plugin_registrant.cc | 2 ++ linux/flutter/generated_plugin_registrant.h | 2 ++ pubspec.lock | 8 ++++---- windows/flutter/generated_plugin_registrant.cc | 2 ++ windows/flutter/generated_plugin_registrant.h | 2 ++ 15 files changed, 50 insertions(+), 13 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 902c84b..89bb1d4 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,5 +1,6 @@ { "@@locale": "de", + "accepted": "", "acceptGroupBtn": "Annehmen", "acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen", "acknowledgedLabel": "bestätigt", @@ -19,6 +20,7 @@ "chatBtn": "Chat", "chatHistoryDefault": "", "contactAlreadyExists": "", + "contactSuggestion": "", "conversationSettings": "", "copiedClipboardNotification": "in die Zwischenablage kopiert", "copiedToClipboardNotification": "in die Zwischenablage kopiert", @@ -121,6 +123,7 @@ "radioNoPassword": "Unverschlüsselt (kein Passwort)", "radioUsePassword": "Passwort", "reallyLeaveThisGroupPrompt": "", + "rejected": "", "rejectGroupBtn": "Ablehnen", "saveBtn": "Speichern", "savePeerHistory": "Peer-Verlauf speichern", @@ -128,6 +131,7 @@ "saveProfileBtn": "Profil speichern", "search": "Suche...", "searchList": "", + "sendAnInvitation": "", "server": "Server", "serverConnectivityConnected": "Server verbunden", "serverConnectivityDisconnected": "Server getrennt", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2f37d53..35e3731 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,5 +1,6 @@ { "@@locale": "en", + "accepted": "Accepted!", "acceptGroupBtn": "Accept", "acceptGroupInviteLabel": "Do you want to accept the invitation to", "acknowledgedLabel": "Acknowledged", @@ -19,6 +20,7 @@ "chatBtn": "Chat", "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", "contactAlreadyExists": "Contact Already Exists", + "contactSuggestion": "This is a contact suggestion for: ", "conversationSettings": "Conversation Settings", "copiedClipboardNotification": "Copied to clipboard", "copiedToClipboardNotification": "Copied to Clipboard", @@ -121,6 +123,7 @@ "radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "Password", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", + "rejected": "Rejected!", "rejectGroupBtn": "Reject", "saveBtn": "Save", "savePeerHistory": "Save Peer History", @@ -128,6 +131,7 @@ "saveProfileBtn": "Save Profile", "search": "Search...", "searchList": "Search List", + "sendAnInvitation": "You sent an invitation for: ", "server": "Server", "serverConnectivityConnected": "Server Connected", "serverConnectivityDisconnected": "Server Disconnected", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index f4ad2b9..7e0ad97 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,5 +1,6 @@ { "@@locale": "es", + "accepted": "", "acceptGroupBtn": "Aceptar", "acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ", "acknowledgedLabel": "Reconocido", @@ -19,6 +20,7 @@ "chatBtn": "Chat", "chatHistoryDefault": "", "contactAlreadyExists": "", + "contactSuggestion": "", "conversationSettings": "", "copiedClipboardNotification": "Copiado al portapapeles", "copiedToClipboardNotification": "Copiado al portapapeles", @@ -121,6 +123,7 @@ "radioNoPassword": "Sin cifrado (sin contraseña)", "radioUsePassword": "Contraseña", "reallyLeaveThisGroupPrompt": "", + "rejected": "", "rejectGroupBtn": "Rechazar", "saveBtn": "Guardar", "savePeerHistory": "Guardar el historial con contacto", @@ -128,6 +131,7 @@ "saveProfileBtn": "Guardar perfil", "search": "Búsqueda...", "searchList": "Buscar en la lista", + "sendAnInvitation": "", "server": "Servidor", "serverConnectivityConnected": "Servidor conectado", "serverConnectivityDisconnected": "Servidor desconectado", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 6df2f99..5e6c18f 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,5 +1,6 @@ { "@@locale": "fr", + "accepted": "", "acceptGroupBtn": "Accepter", "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe", "acknowledgedLabel": "Confirmé", @@ -19,6 +20,7 @@ "chatBtn": "Discuter", "chatHistoryDefault": "", "contactAlreadyExists": "", + "contactSuggestion": "", "conversationSettings": "", "copiedClipboardNotification": "Copié dans le presse-papier", "copiedToClipboardNotification": "Copié dans le presse-papier", @@ -121,6 +123,7 @@ "radioNoPassword": "", "radioUsePassword": "", "reallyLeaveThisGroupPrompt": "", + "rejected": "", "rejectGroupBtn": "Refuser", "saveBtn": "Sauvegarder", "savePeerHistory": "", @@ -128,6 +131,7 @@ "saveProfileBtn": "", "search": "", "searchList": "", + "sendAnInvitation": "", "server": "", "serverConnectivityConnected": "", "serverConnectivityDisconnected": "", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index fee6328..44a26cc 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,5 +1,6 @@ { "@@locale": "it", + "accepted": "", "acceptGroupBtn": "Accetta", "acceptGroupInviteLabel": "Vuoi accettare l'invito a", "acknowledgedLabel": "Riconosciuto", @@ -19,6 +20,7 @@ "chatBtn": "Chat", "chatHistoryDefault": "", "contactAlreadyExists": "", + "contactSuggestion": "", "conversationSettings": "", "copiedClipboardNotification": "Copiato negli Appunti", "copiedToClipboardNotification": "Copiato negli Appunti", @@ -121,6 +123,7 @@ "radioNoPassword": "Non criptato (senza password)", "radioUsePassword": "Password", "reallyLeaveThisGroupPrompt": "", + "rejected": "", "rejectGroupBtn": "Rifiuta", "saveBtn": "Salva", "savePeerHistory": "Salva cronologia peer", @@ -128,6 +131,7 @@ "saveProfileBtn": "Salva il profilo", "search": "Ricerca...", "searchList": "Cerca nella lista", + "sendAnInvitation": "", "server": "Server", "serverConnectivityConnected": "Server connesso", "serverConnectivityDisconnected": "Server disconnesso", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 772eb42..acfbd49 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,5 +1,6 @@ { "@@locale": "pt", + "accepted": "", "acceptGroupBtn": "Aceitar", "acceptGroupInviteLabel": "Você quer aceitar o convite para", "acknowledgedLabel": "Confirmada", @@ -19,6 +20,7 @@ "chatBtn": "Chat", "chatHistoryDefault": "", "contactAlreadyExists": "", + "contactSuggestion": "", "conversationSettings": "", "copiedClipboardNotification": "Copiado", "copiedToClipboardNotification": "Copiado", @@ -121,6 +123,7 @@ "radioNoPassword": "", "radioUsePassword": "", "reallyLeaveThisGroupPrompt": "", + "rejected": "", "rejectGroupBtn": "Recusar", "saveBtn": "Salvar", "savePeerHistory": "", @@ -128,6 +131,7 @@ "saveProfileBtn": "", "search": "", "searchList": "", + "sendAnInvitation": "", "server": "", "serverConnectivityConnected": "", "serverConnectivityDisconnected": "", diff --git a/lib/opaque.dart b/lib/opaque.dart index 5615486..6f8074d 100644 --- a/lib/opaque.dart +++ b/lib/opaque.dart @@ -1383,6 +1383,8 @@ ThemeData mkThemeData(Settings opaque) { )), ), ), + scrollbarTheme: ScrollbarThemeData( + isAlwaysShown: false, thumbColor: MaterialStateProperty.all(opaque.current().scrollbarActiveColor()), trackColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor())), tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor()))), dialogTheme: DialogTheme( backgroundColor: opaque.current().backgroundPaneColor(), diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 10d0569..232fde3 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -64,23 +64,24 @@ class InvitationBubbleState extends State { } var wdgMessage = fromMe - ? senderInviteChrome("You sent an invitation for", isGroup ? "a group" : Provider.of(context).message, myKey) - : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : "This is a contact suggestion for:", Provider.of(context).inviteNick, + ? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation, + isGroup ? Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget)!.nickname : Provider.of(context).message, myKey) + : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, Provider.of(context).inviteNick, Provider.of(context).inviteTarget, myKey)); Widget wdgDecorations; if (fromMe) { wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); } else if (isAccepted) { - wdgDecorations = Text("Accepted!" + '\u202F'); + wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F'); } else if (this.rejected) { - wdgDecorations = Text("Rejected." + '\u202F'); + wdgDecorations = Text(AppLocalizations.of(context)!.rejected + '\u202F'); } else { wdgDecorations = Center( widthFactor: 1, child: Wrap(children: [ - Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Reject" + '\u202F'), onPressed: _btnReject)), - Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Accept" + '\u202F'), onPressed: _btnAccept)), + Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text(AppLocalizations.of(context)!.rejectGroupBtn + '\u202F'), onPressed: _btnReject)), + Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text(AppLocalizations.of(context)!.acceptGroupBtn + '\u202F'), onPressed: _btnAccept)), ])); } diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 2b424d4..5a8fc0f 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -39,7 +39,6 @@ class _MessageListState extends State { )), Expanded( child: Scrollbar( - isAlwaysShown: true, controller: ctrlr1, child: Container( // Only show broken heart is the contact is offline... @@ -54,7 +53,7 @@ class _MessageListState extends State { child: ListView.builder( controller: ctrlr1, itemCount: Provider.of(outerContext).totalMessages, - reverse: true, + reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... itemBuilder: (itemBuilderContext, index) { var trueIndex = Provider.of(outerContext).totalMessages - index - 1; return ChangeNotifierProvider( diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 8f02b96..b6c9978 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -94,7 +94,10 @@ class _MessageRowState extends State { final setPeerAttributeJson = jsonEncode(setPeerAttribute); Provider.of(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson); - final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact)); + final snackBar = SnackBar( + content: Text(AppLocalizations.of(context)!.successfullAddedContact), + duration: Duration(seconds: 2), + ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 99da0ef..9f8c703 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #include "generated_plugin_registrant.h" #include diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h index 9bf7478..e0f0a47 100644 --- a/linux/flutter/generated_plugin_registrant.h +++ b/linux/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/pubspec.lock b/pubspec.lock index 9624a17..3db29ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.7.0" boolean_selector: dependency: transitive description: @@ -191,7 +191,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.0" nested: dependency: transitive description: @@ -392,7 +392,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.0" typed_data: dependency: transitive description: @@ -450,7 +450,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.1" + version: "5.1.2" sdks: dart: ">=2.13.0 <3.0.0" flutter: ">=1.20.0" diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 9deaa4e..9372fc5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #include "generated_plugin_registrant.h" #include diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h index 9846246..dc139d8 100644 --- a/windows/flutter/generated_plugin_registrant.h +++ b/windows/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ From 0e8efc0b6b17fb558eb57f2b03474725903e42f5 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 8 Jun 2021 22:27:35 -0700 Subject: [PATCH 08/17] drone linux dont dup icudtl.dat; windows add recuire VC Redist dlls --- .drone.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 2a873a0..92c5e91 100644 --- a/.drone.yml +++ b/.drone.yml @@ -59,7 +59,8 @@ steps: - cp linux/cwtch.desktop deploy/linux - cp linux/cwtch.png deploy/linux - cp linux/libCwtch.so deploy/linux/lib/ - - cp /sdks/flutter/bin/cache/artifacts/engine/linux-x64/icudtl.dat deploy/linux + # should not be needed, should be in data/flutter_assets and work from there + #- cp /sdks/flutter/bin/cache/artifacts/engine/linux-x64/icudtl.dat deploy/linux - cp tor deploy/linux - cd deploy - mv linux cwtch @@ -230,6 +231,12 @@ steps: - mkdir deploy - move build\\windows\\runner\\Release $Env:builddir - copy windows\libCwtch.dll $Env:builddir + # flutter hasn't worked out it's packaging of required dll's so we have to resort to this manual nonsense + # https://github.com/google/flutter-desktop-embedding/issues/587 + # https://github.com/flutter/flutter/issues/53167 + - copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140.dll $Env:builddir + - copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140_1.dll $Env:builddir + - copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\msvcp140.dll $Env:builddir - powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:builddir\Tor" - powershell -command "Compress-Archive -Path $Env:builddir -DestinationPath $Env:zip" - powershell -command "(Get-FileHash *.zip -Algorithm sha512).Hash" > $Env:sha From b3dd5e2fee74449dfc0a653db3b1bb778dd45035 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 9 Jun 2021 12:57:22 -0700 Subject: [PATCH 09/17] Persist rejection action --- LIBCWTCH-GO.version | 2 +- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 7 +++++ assets/fonts/CwtchIcons.ttf | Bin 20140 -> 19920 bytes lib/cwtch/cwtch.dart | 4 +++ lib/cwtch/cwtchNotifier.dart | 9 +++--- lib/cwtch/ffi.dart | 13 ++++++++ lib/cwtch/gomobile.dart | 7 +++++ lib/cwtch_icons_icons.dart | 5 +-- lib/model.dart | 10 +++++- lib/views/addcontactview.dart | 6 +++- lib/views/addeditprofileview.dart | 7 ++++- lib/widgets/buttontextfield.dart | 1 + lib/widgets/invitationbubble.dart | 29 +++++++++--------- pubspec.yaml | 2 +- 14 files changed, 76 insertions(+), 26 deletions(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 8e40226..d12319f 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -v0.0.2-49-g6a0e839-2021-06-02-19-40 \ No newline at end of file +v0.0.2-58-gfddfd41-2021-06-10-18-36 \ No newline at end of file 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 5066757..dc34259 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -129,6 +129,13 @@ class MainActivity: FlutterActivity() { val end = (call.argument("end") as? Long) ?: 0; result.success(Cwtch.getMessages(profile, handle, start, end)) } + "UpdateMessageFlags" -> { + val profile = (call.argument("profile") as? String) ?: ""; + val handle = (call.argument("contact") as? String) ?: ""; + val midx = (call.argument("midx") as? Long) ?: 0; + val flags = (call.argument("flags") as? Long) ?: 0; + Cwtch.updateMessageFlags(profile, handle, midx, flags); + } "AcceptContact" -> { val profile = (call.argument("ProfileOnion") as? String) ?: ""; val handle = (call.argument("handle") as? String) ?: ""; diff --git a/assets/fonts/CwtchIcons.ttf b/assets/fonts/CwtchIcons.ttf index ef83f2ec4b529c35bf8df8aef5dd16aada8c0846..9a6e5cbd392d7e8f745d942b10720f3e9a054aaa 100644 GIT binary patch delta 1165 zcmX|BO>7%g5T5sTy&K2zx_0)*-q_o$y>^myvQ6yRN!!>-<+cP7g;Kx`6>TtyTenJL zlZ4VpNJfQ9ZQ4q0eWT(+g%nkx2Q0y*0wL532q?Xl0|x>|LWl$0NX%?pc&ktI^Ua%i zZ{}$?KOm3pkeh?m4}X3CZ2)ErfJ>iUSUdCCz?XS|b_3wROsQCy4?ccX2Y{at=@L5l z46}jy1B6jpT)p`GpUox!`57^@P@XGXr&Sifxr+S##lpo3St6sz`O)rPDl8T^?%WIk z9JXr?R>~{x)px(XaTVa1O+0{c>p>)!2ZFJPTb^=?9@6CNjNE+1e=6c|zOu+b3XlwSJWC08|4Oe#adhyBXNVvBvR6 zzHy~-&Av|qo?7d+_5UImz1jL|>&(ZFm$4{UJH-A|JI3;!4m*yuZ)&f6eai7YbMQDO zL%<6tv9UJ5_FZWKUPQUx0=$IsS_^=wYm?58TDWC>bnS<$FKEz*-hgax9}5sb6h;aHDF z_~CQ{ZI0(+ojTHzSJX&SPh?5TJ8Y3ok&Ecz80zVyUe$;c4RaX6?Ygpdpo|gJ=$q3O zRaH77{%LWg@5oz%XU`swG$Bh;;D{`Eh!7Av+zhewK%dv$laRf^(1=%3{Bq<>rrqtN zgfea7*3L{cYZ%!m+1Qm~Q`>n(i^nyx5i+;$YSp+;_1W}SZlBLho&?=dC37gFFy7v- z8Ts_k{>vUA))x~3R1rnLL+uC~Vb#ToUPiY_XmF^TbqCrwIUwyH8hIf~`!*lE{pi%WKJ@wPn*V*!~`p ze3-hrt#-41E1Mfvs=-iTJeQFbg$V|Hdu2aQo#LtUQ=Zi8C0*%0JZbbX#?E6;VxPag zSJmRQWSO2w)Yoenr;8``ozL%1yg^)2CO0t@BC5Yj#YCZia&Y2+=<~8+MfE4r?y2*q z%q}y~=AG=GDV*H-LlQ(QsmC=1ALTpaKh(E!k64ngeL8uMk`wjUo>d)VLScS>xwx`2 tJ6Eo(4J7!drfa3RG(T`|>B70yqHCd8xKJE8yIg*=(%#bNN(KB={0n&p^xpsg delta 1388 zcmYjNU1%It6h3F}oqKm@W_M&H&D^$AqBO`t7XluO(j1| z5^F2MR#0df3YC=#7T>fMX&)pn3dL6?f>uGqP^5?tsSouA#mf`T-OZu50oWk`VVzn!_rlCCuYC&;{}tfz#$vr%8~hyK0f4(CsK!i0X$iw zo)eAL^~A={#}AAF%)CVdq~E-idtx4tVoBD-aJl%y`4ozX&q*%X&!+9ZD5f#PFe#{IB@ue!UbsrW$)>|t#FM!*t-yF zI!L~2t!wNiC5l#+|K4iwxM_qg9=&ADZ@;L1#fldwkp!M0z1bCbmh{E0zzNb@U4gmQ zM*MwQ8hkLcHuUlKe(lm!t9bNw(+23Y*cJ8&%)&gp3a`T@xB~CNNAM}^z<2N?+=oBV z#!>VE4&=R{f_blm(wi!WTFB_}ljzD^UdSXmZg2Sbj$01gUO`p{%fUg%b%M#jub>y& zlT=2QgL2SxTNGmPXd2fiyzbSc@Dc`t)5^_#{dnlwZ(j6MZ> z)P|%?AsLi@Sfi4Z?Uv$#I>r%OHj5=}{$8dD7srO-IEMwBBw z;jz&qN}HsFp8Z();3&;Qd)xa&ZZw;pil9j+^hSZ?9j#!%hM81UAvJTn$2L?;8|XK< zo*vL_HEFYE&zPx&+pAKu`g(0EA?zqZGo)}#Q%Ts&>glsJd0)g6NjFs}6d6l9GNz66 zGq%rRF;yEJ!QxNFjy=Ef1q$q6;vuxX`i&yh-r&n zKZ{>j^l|U`xpq10PvmeX=g*O|y%`1B__FO{b2njdt#3fHRNC|*8C|+c|Irl&q!4wA z7*9rYDTt;uW?L~#X#>OzjYkxsi$#poxMol$5%N7TNQA{y zU45RI)(H*Ml?WcE19ex(R984uTOFlepWe_^ZnVbvu$+w+5YqB*Bs|tUmp8# z`<0n~by8oiuhfdCSI(T?s2dBbjdR6nt+rNQU)PuF)id?tskPOY8d}$0SgdXU{0;o> B;#B|u diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index cb2b0cd..4f96d6b 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + abstract class Cwtch { // ignore: non_constant_identifier_names Future Start(); @@ -38,6 +40,8 @@ abstract class Cwtch { // ignore: non_constant_identifier_names Future GetMessage(String profile, String handle, int index); // ignore: non_constant_identifier_names + void UpdateMessageFlags(String profile, String handle, int index, int flags); + // ignore: non_constant_identifier_names Future GetMessages(String profile, String handle, int start, int end); // ignore: non_constant_identifier_names void SendMessage(String profile, String handle, String message); diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 021f94c..950e21c 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -178,23 +178,24 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]); break; case "NewGroup": - print("new group invite: $data"); + print("new group: $data"); String invite = data["GroupInvite"].toString(); if (invite.startsWith("torv3")) { String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5))); dynamic groupInvite = jsonDecode(inviteJson); - print("new group invite: $groupInvite"); + print("group invite: $groupInvite"); // Retrieve Server Status from Cache... String status = ""; - ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(groupInvite["ServerHost"]); + ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]); if (serverInfoState != null) { + print("Got server status: " + serverInfoState.status); status = serverInfoState.status; } if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) { profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"], - isInvitation: true, + isInvitation: false, imagePath: data["PicturePath"], nickname: groupInvite["GroupName"], server: groupInvite["ServerHost"], diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 410ffba..df0e163 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -27,6 +27,9 @@ typedef VoidFromStringStringStringFn = void Function(Pointer, int, Pointer typedef void_from_string_string_string_string_function = Void Function(Pointer, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32); typedef VoidFromStringStringStringStringFn = void Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int); +typedef void_from_string_string_int_int_function = Void Function(Pointer, Int32, Pointer, Int32, Int64, Int64); +typedef VoidFromStringStringIntIntFn = void Function(Pointer, int, Pointer, int, int, int); + typedef access_cwtch_eventbus_function = Void Function(); typedef NextEventFn = void Function(); @@ -384,4 +387,14 @@ class CwtchFfi implements Cwtch { final u2 = groupHandle.toNativeUtf8(); RejectInvite(u1, u1.length, u2, u2.length); } + + @override + void UpdateMessageFlags(String profile, String handle, int index, int flags) { + var updateMessageFlagsC = library.lookup>("c_UpdateMessageFlags"); + // ignore: non_constant_identifier_names + final updateMessageFlags = updateMessageFlagsC.asFunction(); + final utf8profile = profile.toNativeUtf8(); + final utf8handle = handle.toNativeUtf8(); + updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags); + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 210eb42..aba1f6e 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ffi'; import 'dart:io'; import 'package:cwtch/config.dart'; @@ -187,4 +188,10 @@ class CwtchGomobile implements Cwtch { void LeaveGroup(String profileOnion, String groupHandle) { cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "handle": groupHandle}); } + + @override + void UpdateMessageFlags(String profile, String handle, int index, int flags) { + print("gomobile.dart UpdateMessageFlags " + index.toString()); + cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "index": index, "flags": flags}); + } } diff --git a/lib/cwtch_icons_icons.dart b/lib/cwtch_icons_icons.dart index 8552f4d..6c89b9a 100644 --- a/lib/cwtch_icons_icons.dart +++ b/lib/cwtch_icons_icons.dart @@ -1,4 +1,4 @@ -/// Flutter icons MyFlutterApp +/// Flutter icons CwtchIcons /// Copyright (C) 2021 by original authors @ fluttericon.com, fontello.com /// This font was generated by FlutterIcon.com, which is derived from Fontello. /// @@ -102,8 +102,9 @@ class CwtchIcons { static const IconData add_group = IconData(0xe84e, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData add_peer = IconData(0xe84f, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData add_24px = IconData(0xe850, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData address_copy_2 = IconData(0xe852, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData address = IconData(0xe856, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData send_invite = IconData(0xe888, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData copy_address = IconData(0xe889, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData leave_group = IconData(0xe88a, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData leave_chat = IconData(0xe88b, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/lib/model.dart b/lib/model.dart index a07fb07..7dec94b 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -183,7 +183,7 @@ class ProfileInfoState extends ChangeNotifier { // Parse out the server list json into our server info state struct... void replaceServers(String serversJson) { - if (serversJson != null && serversJson != "" && serversJson != "null") { + if (serversJson != "" && serversJson != "null") { print("got servers $serversJson"); List servers = jsonDecode(serversJson); this._servers.replace(servers.map((server) { @@ -382,6 +382,7 @@ class MessageState extends ChangeNotifier { late String _inviteNick; late DateTime _timestamp; late String _senderOnion; + late int _flags; String? _senderImage; late String _signature = ""; late bool _ackd = false; @@ -402,6 +403,12 @@ class MessageState extends ChangeNotifier { get message => this._message; get overlay => this._overlay; get timestamp => this._timestamp; + int get flags => this._flags; + set flags(int newVal) { + this._flags = newVal; + notifyListeners(); + } + bool get ackd => this._ackd; bool get error => this._error; bool get malformed => this._malformed; @@ -450,6 +457,7 @@ class MessageState extends ChangeNotifier { this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; this._senderOnion = messageWrapper['PeerID']; this._senderImage = messageWrapper['ContactImage']; + this._flags = int.parse(messageWrapper['Flags'].toString(), radix: 2); // If this is a group, store the signature if (contactHandle.length == 32) { diff --git a/lib/views/addcontactview.dart b/lib/views/addcontactview.dart index 3d9bc61..3c27c64 100644 --- a/lib/views/addcontactview.dart +++ b/lib/views/addcontactview.dart @@ -119,7 +119,11 @@ class _AddContactViewState extends State { CwtchButtonTextField( controller: ctrlrOnion, onPressed: _copyOnion, - icon: Icon(CwtchIcons.copy_address), + readonly: true, + icon: Icon( + CwtchIcons.address_copy_2, + size: 32, + ), tooltip: AppLocalizations.of(context)!.copyBtn, ), SizedBox( diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index c27308c..7fcc3b4 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -11,6 +11,7 @@ import 'package:cwtch/widgets/textfield.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../cwtch_icons_icons.dart'; import '../main.dart'; import '../opaque.dart'; import '../settings.dart'; @@ -122,7 +123,11 @@ class _AddEditProfileViewState extends State { CwtchButtonTextField( controller: ctrlrOnion, onPressed: _copyOnion, - icon: Icon(Icons.copy), + readonly: true, + icon: Icon( + CwtchIcons.address_copy_2, + size: 32, + ), tooltip: AppLocalizations.of(context)!.copyBtn, ) ])), diff --git a/lib/widgets/buttontextfield.dart b/lib/widgets/buttontextfield.dart index 83083fb..65f8fa5 100644 --- a/lib/widgets/buttontextfield.dart +++ b/lib/widgets/buttontextfield.dart @@ -28,6 +28,7 @@ class _CwtchButtonTextFieldState extends State { suffixIcon: IconButton( onPressed: widget.onPressed, icon: widget.icon, + padding: EdgeInsets.fromLTRB(0.0, 4.0, 2.0, 2.0), tooltip: widget.tooltip, enableFeedback: true, color: theme.current().mainTextColor(), diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 232fde3..7913f38 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -22,15 +22,17 @@ class InvitationBubble extends StatefulWidget { class InvitationBubbleState extends State { bool rejected = false; + bool isAccepted = false; FocusNode _focus = FocusNode(); @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderOnion == Provider.of(context).onion; var isGroup = Provider.of(context).overlay == 101; - var isAccepted = Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget) != null; + isAccepted = Provider.of(context).contactList.getContact(Provider.of(context).inviteTarget) != null; var prettyDate = ""; var borderRadiousEh = 15.0; + rejected = Provider.of(context).flags & 0x01 == 0x01; var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); if (Provider.of(context).timestamp != null) { @@ -54,8 +56,6 @@ class InvitationBubbleState extends State { child: SelectableText(senderDisplayStr + '\u202F', style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor()))); - // todo: translations - // If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from // some kind of malfeasance. var selfInvite = Provider.of(context).inviteNick == Provider.of(context).onion; @@ -120,22 +120,21 @@ class InvitationBubbleState extends State { } void _btnReject() { - //todo: how should we track inline invite rejections? - setState(() => this.rejected = true); + setState(() { + var profileOnion = Provider.of(context, listen: false).onion; + var contact = Provider.of(context, listen: false).onion; + var idx = Provider.of(context, listen: false).messageIndex; + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); + Provider.of(context).flags |= 0x01; + }); } void _btnAccept() { - var profileOnion = Provider.of(context, listen: false).onion; - if (Provider.of(context, listen: false).overlay == 100) { - final setPeerAttribute = { - "EventType": "AddContact", - "Data": {"ImportString": Provider.of(context, listen: false).message}, - }; - final setPeerAttributeJson = jsonEncode(setPeerAttribute); - Provider.of(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson); - } else { + setState(() { + var profileOnion = Provider.of(context, listen: false).onion; Provider.of(context, listen: false).cwtch.ImportBundle(profileOnion, Provider.of(context, listen: false).message); - } + isAccepted = true; + }); } // Construct an invite chrome for the sender diff --git a/pubspec.yaml b/pubspec.yaml index b232973..45e488b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+10 +version: 1.0.0+11 environment: sdk: ">=2.12.0 <3.0.0" From 0d56c5a3bd63366b763447040e243e244b6b4bc1 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 10 Jun 2021 14:12:37 -0700 Subject: [PATCH 10/17] Erinns Comments --- lib/cwtch/cwtch.dart | 2 -- lib/cwtch/gomobile.dart | 2 -- lib/cwtch_icons_icons.dart | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 4f96d6b..816fa65 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -1,5 +1,3 @@ -import 'dart:ffi'; - abstract class Cwtch { // ignore: non_constant_identifier_names Future Start(); diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index aba1f6e..d89f38e 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -1,6 +1,4 @@ import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io'; import 'package:cwtch/config.dart'; import 'package:flutter/services.dart'; diff --git a/lib/cwtch_icons_icons.dart b/lib/cwtch_icons_icons.dart index 6c89b9a..8722310 100644 --- a/lib/cwtch_icons_icons.dart +++ b/lib/cwtch_icons_icons.dart @@ -1,5 +1,5 @@ /// Flutter icons CwtchIcons -/// Copyright (C) 2021 by original authors @ fluttericon.com, fontello.com +/// Copyright (C) 2021 by Open Privacy Research Society via fluttericon.com, fontello.com /// This font was generated by FlutterIcon.com, which is derived from Fontello. /// /// To use this font, place it in your fonts/ directory and include the From 9c69275fe64f6ab8eb29ac806fe8970e8559375c Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 14:28:20 -0700 Subject: [PATCH 11/17] android service and notification support take one. ACTION --- android/app/build.gradle | 5 +- android/app/src/main/AndroidManifest.xml | 3 + .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 145 ++++++++++++------ .../kotlin/im/cwtch/flwtch/MainActivity.kt | 145 +++++++++++++----- lib/cwtch/cwtch.dart | 4 +- lib/cwtch/ffi.dart | 14 +- lib/cwtch/gomobile.dart | 10 +- lib/main.dart | 37 ++++- lib/model.dart | 1 + 9 files changed, 268 insertions(+), 96 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 91220d1..58459a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,6 +114,7 @@ dependencies { // end of workmanager deps - // ipc - implementation "io.reactivex:rxkotlin:1.x.y" + // needed to prevent a ListenableFuture dependency conflict/bug + // see https://github.com/google/ExoPlayer/issues/7905#issuecomment-692637059 + implementation 'com.google.guava:guava:any' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a0ea053..ec51df3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,4 +45,7 @@ + + + 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 1ff7a92..97c3024 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -1,21 +1,19 @@ package im.cwtch.flwtch -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager +import android.app.* import android.content.Context +import android.content.Context.ACTIVITY_SERVICE +import android.content.Intent +import android.graphics.BitmapFactory import android.graphics.Color import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.* import cwtch.Cwtch -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import io.flutter.FlutterInjector import org.json.JSONObject @@ -27,6 +25,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private var notificationID: MutableMap = mutableMapOf() + private var notificationIDnext: Int = 1; + override suspend fun doWork(): Result { val method = inputData.getString(KEY_METHOD) ?: return Result.failure() @@ -36,38 +37,96 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : // Mark the Worker as important val progress = "Trying to do a Flwtch" setForeground(createForegroundInfo(progress)) - handleCwtch(method, args) - return Result.success() + return handleCwtch(method, args) } - private suspend fun handleCwtch(method: String, args: String) { - var a = JSONObject(args); + private fun getNotificationID(profile: String, contact: String): Int { + val k = "$profile $contact" + if (!notificationID.containsKey(k)) { + notificationID[k] = notificationIDnext++ + } + return notificationID[k] ?: -1 + } + + private suspend fun handleCwtch(method: String, args: String): Result { + val a = JSONObject(args); when (method) { "Start" -> { Log.i("FlwtchWorker.kt", "handleAppInfo Start") - val appDir = (a.get("appDir") as? String) ?: ""; - val torPath = (a.get("torPath") as? String) ?: "tor"; - Log.i("FlwtchWorker.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'") + val appDir = (a.get("appDir") as? String) ?: "" + val torPath = (a.get("torPath") as? String) ?: "tor" + Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'") - Cwtch.startCwtch(appDir, torPath) + if (Cwtch.startCwtch(appDir, torPath) != 0.toByte()) return Result.failure() // infinite coroutine :) while(true) { val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent()) - Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) + //Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) if (isStopped) { Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") } - //todo: this elides evt.EventID which may be needed at some point? - val data = Data.Builder() - .putString("EventType", evt.EventType) - .putString("Data", evt.Data) - .putString("EventID", evt.EventID) + + if (evt.EventType == "NewMessageFromPeer") { + val data = JSONObject(evt.Data) + +// val appProcesses: List = (applicationContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager).runningAppProcesses +// for (appProcess in appProcesses) { +// if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { +// Log.i("Foreground App", appProcess.processName) +// } +// } + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createMessageNotificationChannel(data.getString("RemotePeer"), data.getString("RemotePeer")) + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + val loader = FlutterInjector.instance().flutterLoader() + val key = loader.getLookupKeyForAsset("assets/"+data.getString("Picture"))//"assets/profiles/001-centaur.png") + val fh = applicationContext.assets.open(key) + + val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent -> + intent.setAction(Intent.ACTION_RUN)//("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") + //intent.setClassName("im.cwtch.flwtch", "MainActivity") + intent.putExtra("EventType", "NotificationClicked") + intent.putExtra("ProfileOnion", data.getString("ProfileOnion")) + intent.putExtra("RemotePeer", data.getString("RemotePeer")) + } + + val newNotification = NotificationCompat.Builder(applicationContext, channelId) + .setContentTitle(data.getString("Nick")) + .setContentText("New message") + .setLargeIcon(BitmapFactory.decodeStream(fh)) + .setSmallIcon(R.mipmap.knott) + .setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + .setAutoCancel(true) .build() - setProgress(data) - Thread.sleep(200) + notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification) + } + //todo: this elides evt.EventID which may be needed at some point? +// val data = Data.Builder() +// .putString("EventType", evt.EventType) +// .putString("Data", evt.Data) +// .putString("EventID", evt.EventID) +// .build() + //setProgress(data)//progress can only hold a single undelivered value so it's possible for observers to miss rapidfire updates + //Thread.sleep(200)//this is a kludge to make it mostly-work until a proper channel is implemented + Intent().also { intent -> + intent.setAction("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") + intent.putExtra("EventType", evt.EventType) + intent.putExtra("Data", evt.Data) + intent.putExtra("EventID", evt.EventID) + LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent) + } } } + "ReconnectCwtchForeground" -> { + Cwtch.reconnectCwtchForeground() + } "SelectProfile" -> { val onion = (a.get("profile") as? String) ?: ""; Cwtch.selectProfile(onion) @@ -87,7 +146,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : "NumMessages" -> { val profile = (a.get("profile") as? String) ?: ""; val handle = (a.get("contact") as? String) ?: ""; - Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) + return Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) } "GetMessage" -> { //Log.i("MainActivivity.kt", (a.get("index"))); @@ -100,14 +159,14 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val handle = (a.get("contact") as? String) ?: ""; val indexI = a.getInt("index") ?: 0; Log.i("FlwtchWorker.kt", "indexI = " + indexI) - Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) + return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) } "GetMessages" -> { val profile = (a.get("profile") as? String) ?: ""; val handle = (a.get("contact") as? String) ?: ""; val start = (a.get("start") as? Long) ?: 0; val end = (a.get("end") as? Long) ?: 0; - Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) + return Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) } "AcceptContact" -> { val profile = (a.get("ProfileOnion") as? String) ?: ""; @@ -177,26 +236,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val groupHandle = (a.get("groupHandle") as? String) ?: ""; Cwtch.rejectInvite(profile, groupHandle); } - else -> Result.failure() - } - } - - private suspend fun launchCwtch(appDir: String, torPath: String) { - Cwtch.startCwtch(appDir, torPath) - // seperate coroutine to poll event bus and send to dart - //Log.i("FlwtchWorker.kt", "got event chan: " + eventbus_chan + " launching corouting...") - GlobalScope.launch(Dispatchers.IO) { - while(true) { - val evt = AppbusEvent(Cwtch.getAppBusEvent()) - Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) - launch(Dispatchers.Main) { - //todo: this elides evt.EventID which may be needed at some point? - val flutterEngine = FlutterEngine(applicationContext) - val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) - eventbus_chan.invokeMethod(evt.EventType, evt.Data) - } - } + else -> return Result.failure() } + return Result.success() } // Creates an instance of ForegroundInfo which can be used to update the @@ -243,6 +285,17 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : return channelId } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createMessageNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_HIGH) + chan.lightColor = Color.MAGENTA + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + notificationManager.createNotificationChannel(chan) + return channelId + } + companion object { const val KEY_METHOD = "KEY_METHOD" const val KEY_ARGS = "KEY_ARGS" 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 d50dc49..3820914 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -1,12 +1,20 @@ package im.cwtch.flwtch import SplashView +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import androidx.annotation.NonNull import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.os.Looper import android.util.Log +import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.lifecycle.Observer +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.* import kotlinx.coroutines.Dispatchers @@ -27,6 +35,8 @@ import kotlin.concurrent.thread import org.json.JSONObject import java.io.File +import java.time.Duration +import java.util.concurrent.TimeUnit class MainActivity: FlutterActivity() { @@ -42,11 +52,32 @@ class MainActivity: FlutterActivity() { // Channel to send eventbus events on private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus" + // Channel to trigger contactview when an external notification is clicked + private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler" + + private var methodChan: MethodChannel? = null + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (methodChan == null || intent.extras == null) return + if (!intent.extras!!.containsKey("ProfileOnion") || !intent.extras!!.containsKey("RemotePeer")) { + Log.i("onNewIntent", "got intent with no onions") + return + } + val profile = intent.extras!!.getString("ProfileOnion") + val handle = intent.extras!!.getString("RemotePeer") + val mappo = mapOf("ProfileOnion" to profile, "RemotePeer" to handle) + Log.i("MainActivity.kt", "onNewIntent($profile, $handle)") + val j = JSONObject(mappo) + methodChan!!.invokeMethod("NotificationClicked", j.toString()) + } + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // Note: this methods are invoked on the main thread. MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) } + methodChan = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK) } private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) { @@ -61,49 +92,72 @@ class MainActivity: FlutterActivity() { val ainfo = this.applicationContext.packageManager.getApplicationInfo( "im.cwtch.flwtch", // Must be app name PackageManager.GET_SHARED_LIBRARY_FILES) - return ainfo.nativeLibraryDir } + // receives messages from the ForegroundService (which provides, ironically enough, the backend) private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) { + var method = call.method val argmap: Map = call.arguments as Map - val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() - var tag = "" - if (call.method == "Start") { - tag = "cwtchEventBus" - } - val workRequest: WorkRequest = OneTimeWorkRequestBuilder().setInputData(data).addTag(tag).build() - WorkManager.getInstance(this).enqueue(workRequest) - if (call.method == "Start") { - val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) - WorkManager.getInstance(applicationContext) - // requestId is the WorkRequest id - .getWorkInfosByTagLiveData("cwtchEventBus") - .observe(this, Observer> { listOfWorkInfo -> - if (listOfWorkInfo.isNullOrEmpty()) { - return@Observer - } - for (workInfo in listOfWorkInfo) { - if (workInfo != null) { - val progress = workInfo.progress - val eventType = progress.getString("EventType") ?: "" - val eventData = progress.getString("Data") - val output = progress.keyValueMap.toString() - try { - if (eventType != "") { - Log.i("MainActivity.kt", "got event $output $eventType $eventData") - eventbus_chan.invokeMethod(eventType, eventData) - } - } catch (e: Exception) { - Log.i("MainActivity.kt", "event bus exception") - } - } else { - Log.i("MainActivity.kt", "got null workInfo") - } - } - }) + // the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start() + // in case the ForegroundService is still running. in both cases, however, we *do* want to re-register + // the eventbus listener. + if (call.method == "Start") { + val workerTag = "cwtchEventBusWorker" + val uniqueTag = argmap["torPath"] ?: "nullEventBus" + + // note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get + // accidentally duplicated. however, we still need to manually check if it's running or not, so + // that we can divert this method call to ReconnectCwtchForeground instead if so. + val works = WorkManager.getInstance(this).getWorkInfosByTag(workerTag).get() + var alreadyRunning = false + for (workInfo in works) { + if (workInfo.tags.contains(uniqueTag)) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + alreadyRunning = true + } + } else { + WorkManager.getInstance(this).cancelWorkById(workInfo.id) + } + } + + // register our eventbus listener. note that we observe any/all work according to its tag, which + // results in an implicit "reconnection" to old service threads even after frontend restarts + val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) + val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") + LocalBroadcastManager.getInstance(applicationContext).registerReceiver(MyBroadcastReceiver(mc), filter) + + if (alreadyRunning) { + Log.i("MainActivity.kt", "diverting Start -> Reconnect") + method = "ReconnectCwtchForeground" + } else { + Log.i("MainActivity.kt", "Start() launching foregroundservice") + // this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() + // 15 minutes is the shortest interval you can request + val workRequest = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).setInputData(data).addTag(workerTag).addTag(uniqueTag).build() + val workResult = WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.KEEP, workRequest) + return + } } + + // ...otherwise fallthru to a normal ffi method call (and return the result using the result callback) + val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() + val workRequest = OneTimeWorkRequestBuilder().setInputData(data).build() + val workResult = WorkManager.getInstance(this).enqueue(workRequest) + WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe( + this, Observer { workInfo -> + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + val res = workInfo.outputData.keyValueMap.toString() + //Log.i("MainActivity.kt", "method $method returned SUCCESS($res)") + result.success(workInfo.outputData.getString("result")) + } else { + val idk = workInfo.state.toString() + //Log.i("MainActivity.kt", "method $method returned $idk") + } + } + ) } // source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095 @@ -125,4 +179,23 @@ class MainActivity: FlutterActivity() { val EventID = this.optString("EventID") val Data = this.optString("Data") } + + class MyBroadcastReceiver(mc: MethodChannel) : BroadcastReceiver() { + val eventBus: MethodChannel = mc + + override fun onReceive(context: Context, intent: Intent) { +// StringBuilder().apply { +// append("Action: ${intent.action}\n") +// append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n") +// toString().also { log -> +// Log.d("MyBroadcastReceiver", log) +// } +// } + val evtType = intent.getStringExtra("EventType") ?: "" + val evtData = intent.getStringExtra("Data") ?: "" + //val evtID = intent.getStringExtra("EventID") ?: ""//todo? + eventBus.invokeMethod(evtType, evtData) + } + } + } diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index cb2b0cd..3b22cde 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -1,6 +1,8 @@ abstract class Cwtch { // ignore: non_constant_identifier_names - Future Start(); + Future Start(); + // ignore: non_constant_identifier_names + Future ReconnectCwtchForeground(); // ignore: non_constant_identifier_names void SelectProfile(String onion); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 410ffba..f335178 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -15,8 +15,8 @@ import '../config.dart'; /// Cwtch API /// ///////////////////// -typedef start_cwtch_function = Void Function(Pointer str, Int32 length, Pointer str2, Int32 length2); -typedef StartCwtchFn = void Function(Pointer dir, int len, Pointer tor, int torLen); +typedef start_cwtch_function = Int8 Function(Pointer str, Int32 length, Pointer str2, Int32 length2); +typedef StartCwtchFn = int Function(Pointer dir, int len, Pointer tor, int torLen); typedef void_from_string_string_function = Void Function(Pointer, Int32, Pointer, Int32); typedef VoidFromStringStringFn = void Function(Pointer, int, Pointer, int); @@ -76,7 +76,7 @@ class CwtchFfi implements Cwtch { } // ignore: non_constant_identifier_names - Future Start() async { + Future Start() async { String home = ""; String bundledTor = ""; Map envVars = Platform.environment; @@ -109,6 +109,14 @@ class CwtchFfi implements Cwtch { }); } + // ignore: non_constant_identifier_names + Future ReconnectCwtchForeground() async { + var reconnectCwtch = library.lookup>("c_ReconnectCwtchForeground"); + // ignore: non_constant_identifier_names + final ReconnectCwtchForeground = reconnectCwtch.asFunction(); + ReconnectCwtchForeground(); + } + // Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events @override void dispose() { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 210eb42..940fd0c 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -43,7 +43,7 @@ class CwtchGomobile implements Cwtch { } // ignore: non_constant_identifier_names - Future Start() async { + Future Start() async { print("gomobile.dart: Start()..."); var cwtchDir = path.join((await androidHomeDirectory).path, ".cwtch"); if (EnvironmentConfig.BUILD_VER == dev_version) { @@ -51,7 +51,13 @@ class CwtchGomobile implements Cwtch { } String torPath = path.join(await androidLibraryDir, "libtor.so"); print("gomobile.dart: Start invokeMethod Start($cwtchDir, $torPath)..."); - cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath}); + return cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath}); + } + + @override + // ignore: non_constant_identifier_names + Future ReconnectCwtchForeground() async { + cwtchPlatform.invokeMethod("ReconnectCwtchForeground", {}); } // Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier diff --git a/lib/main.dart b/lib/main.dart index bfde1c1..d2da036 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:cwtch/notification_manager.dart'; +import 'package:cwtch/views/messageview.dart'; import 'package:cwtch/widgets/rightshiftfixer.dart'; import 'package:flutter/foundation.dart'; import 'package:cwtch/cwtch/ffi.dart'; @@ -8,6 +11,7 @@ import 'package:cwtch/errorHandler.dart'; import 'package:cwtch/settings.dart'; import 'package:cwtch/torstatus.dart'; import 'package:cwtch/views/triplecolview.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'cwtch/cwtch.dart'; import 'cwtch/cwtchNotifier.dart'; @@ -44,6 +48,8 @@ class FlwtchState extends State { var columns = [1]; // default or 'single column' mode //var columns = [1, 1, 2]; late ProfileListState profs; + final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler'); + final GlobalKey navKey = GlobalKey(); @override initState() { @@ -51,6 +57,7 @@ class FlwtchState extends State { cwtchInit = false; profs = ProfileListState(); + notificationClickChannel.setMethodCallHandler(_externalNotificationClicked); if (Platform.isAndroid) { var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager()); @@ -63,10 +70,9 @@ class FlwtchState extends State { cwtch = CwtchFfi(cwtchNotifier); } - cwtch.Start().then((val) { - setState(() { - cwtchInit = true; - }); + cwtch.Start(); + setState(() { + cwtchInit = true; }); } @@ -92,13 +98,12 @@ class FlwtchState extends State { return Consumer( builder: (context, settings, child) => MaterialApp( key: Key('app'), + navigatorKey: navKey, locale: settings.locale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, title: 'Cwtch', theme: mkThemeData(settings), - // from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(), - // from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(), home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ShiftRightFixer(child: ProfileMgrView())) : SplashView(), ), ); @@ -106,6 +111,26 @@ class FlwtchState extends State { ); } + Future _externalNotificationClicked(MethodCall call) async { + var args = jsonDecode(call.arguments); + var profile = profs.getProfile(args["ProfileOnion"])!; + var contact = profile.contactList.getContact(args["RemotePeer"])!; + contact.unreadMessages = 0; + navKey.currentState?.push( + MaterialPageRoute( + builder: (BuildContext builderContext) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: profile), + ChangeNotifierProvider.value(value: contact), + ], + builder: (context, child) => MessageView(), + ); + }, + ), + ); + } + @override void dispose() { cwtch.dispose(); diff --git a/lib/model.dart b/lib/model.dart index 2752e26..f0f3ef8 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -426,6 +426,7 @@ class MessageState extends ChangeNotifier { void tryLoad(BuildContext context) { Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { try { + print("debug messageJson $jsonMessage"); dynamic messageWrapper = jsonDecode(jsonMessage); if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { this._senderOnion = profileOnion; From 97f1e43009d6f543e7d7c5d1f2288894043cb73f Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 14:51:35 -0700 Subject: [PATCH 12/17] remove debugging cruft --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 35 ++----------------- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 16 ++------- android/build.gradle | 1 + lib/model.dart | 1 - pubspec.lock | 4 +-- 5 files changed, 8 insertions(+), 49 deletions(-) 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 97c3024..5759fbe 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -19,8 +19,6 @@ import org.json.JSONObject class FlwtchWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { - private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus" - private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -33,9 +31,8 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : ?: return Result.failure() val args = inputData.getString(KEY_ARGS) ?: return Result.failure() - Log.i("FlwtchWorker.kt", "got call $method with args $args") // Mark the Worker as important - val progress = "Trying to do a Flwtch" + val progress = "Trying to do a Flwtch"//todo:translate setForeground(createForegroundInfo(progress)) return handleCwtch(method, args) } @@ -62,20 +59,8 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : // infinite coroutine :) while(true) { val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent()) - //Log.i("FlwtchWorker.kt", "got appbusEvent: " + evt) - if (isStopped) { - Log.i("FlwtchWorker.kt", "COROUTINEWORKER DOT ISSTOPPED TRUE OH MY") - } - if (evt.EventType == "NewMessageFromPeer") { val data = JSONObject(evt.Data) - -// val appProcesses: List = (applicationContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager).runningAppProcesses -// for (appProcess in appProcesses) { -// if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { -// Log.i("Foreground App", appProcess.processName) -// } -// } val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createMessageNotificationChannel(data.getString("RemotePeer"), data.getString("RemotePeer")) @@ -90,8 +75,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val fh = applicationContext.assets.open(key) val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent -> - intent.setAction(Intent.ACTION_RUN)//("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") - //intent.setClassName("im.cwtch.flwtch", "MainActivity") + intent.setAction(Intent.ACTION_RUN) intent.putExtra("EventType", "NotificationClicked") intent.putExtra("ProfileOnion", data.getString("ProfileOnion")) intent.putExtra("RemotePeer", data.getString("RemotePeer")) @@ -107,14 +91,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : .build() notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification) } - //todo: this elides evt.EventID which may be needed at some point? -// val data = Data.Builder() -// .putString("EventType", evt.EventType) -// .putString("Data", evt.Data) -// .putString("EventID", evt.EventID) -// .build() - //setProgress(data)//progress can only hold a single undelivered value so it's possible for observers to miss rapidfire updates - //Thread.sleep(200)//this is a kludge to make it mostly-work until a proper channel is implemented + Intent().also { intent -> intent.setAction("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") intent.putExtra("EventType", evt.EventType) @@ -149,12 +126,6 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : return Result.success(Data.Builder().putLong("result", Cwtch.numMessages(profile, handle)).build()) } "GetMessage" -> { - //Log.i("MainActivivity.kt", (a.get("index"))); - -// var args : HashMap = a.gets(); -// Log.i("FlwtchWorker.kt", args); - - val profile = (a.get("profile") as? String) ?: ""; val handle = (a.get("contact") as? String) ?: ""; val indexI = a.getInt("index") ?: 0; 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 3820914..4db21d5 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -67,7 +67,6 @@ class MainActivity: FlutterActivity() { val profile = intent.extras!!.getString("ProfileOnion") val handle = intent.extras!!.getString("RemotePeer") val mappo = mapOf("ProfileOnion" to profile, "RemotePeer" to handle) - Log.i("MainActivity.kt", "onNewIntent($profile, $handle)") val j = JSONObject(mappo) methodChan!!.invokeMethod("NotificationClicked", j.toString()) } @@ -137,7 +136,7 @@ class MainActivity: FlutterActivity() { val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() // 15 minutes is the shortest interval you can request val workRequest = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).setInputData(data).addTag(workerTag).addTag(uniqueTag).build() - val workResult = WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.KEEP, workRequest) + WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.KEEP, workRequest) return } } @@ -145,16 +144,12 @@ class MainActivity: FlutterActivity() { // ...otherwise fallthru to a normal ffi method call (and return the result using the result callback) val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build() val workRequest = OneTimeWorkRequestBuilder().setInputData(data).build() - val workResult = WorkManager.getInstance(this).enqueue(workRequest) + WorkManager.getInstance(this).enqueue(workRequest) WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe( this, Observer { workInfo -> if (workInfo.state == WorkInfo.State.SUCCEEDED) { val res = workInfo.outputData.keyValueMap.toString() - //Log.i("MainActivity.kt", "method $method returned SUCCESS($res)") result.success(workInfo.outputData.getString("result")) - } else { - val idk = workInfo.state.toString() - //Log.i("MainActivity.kt", "method $method returned $idk") } } ) @@ -184,13 +179,6 @@ class MainActivity: FlutterActivity() { val eventBus: MethodChannel = mc override fun onReceive(context: Context, intent: Intent) { -// StringBuilder().apply { -// append("Action: ${intent.action}\n") -// append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n") -// toString().also { log -> -// Log.d("MyBroadcastReceiver", log) -// } -// } val evtType = intent.getStringExtra("EventType") ?: "" val evtData = intent.getStringExtra("Data") ?: "" //val evtID = intent.getStringExtra("EventID") ?: ""//todo? diff --git a/android/build.gradle b/android/build.gradle index 5685ae0..c887697 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,6 +27,7 @@ subprojects { project.evaluationDependsOn(':app') } +//removed due to gradle namespace conflicts that are beyond erinn's mere mortal understanding //task clean(type: Delete) { // delete rootProject.buildDir //} diff --git a/lib/model.dart b/lib/model.dart index 2100b84..a07fb07 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -436,7 +436,6 @@ class MessageState extends ChangeNotifier { void tryLoad(BuildContext context) { Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { try { - print("debug messageJson $jsonMessage"); dynamic messageWrapper = jsonDecode(jsonMessage); if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { this._senderOnion = profileOnion; diff --git a/pubspec.lock b/pubspec.lock index 9588274..3db29ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,7 +105,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.1" flutter: dependency: "direct main" description: flutter @@ -413,7 +413,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "6.1.0+1" + version: "6.2.0" webdriver: dependency: transitive description: From 5c8d448a37125b439ccae73c6564039ca667129e Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 15:05:38 -0700 Subject: [PATCH 13/17] update lcg version --- LIBCWTCH-GO.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index d12319f..d21cb28 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -v0.0.2-58-gfddfd41-2021-06-10-18-36 \ No newline at end of file +v0.0.2-63-g033de73-2021-06-11-21-41 \ No newline at end of file From 2abdcdeae02be017f79118926dc5668d4772e9a4 Mon Sep 17 00:00:00 2001 From: erinn Date: Fri, 11 Jun 2021 15:15:42 -0700 Subject: [PATCH 14/17] updatemessageflags got mismerged oops --- .../app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 5759fbe..7ab5f2c 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -139,6 +139,13 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val end = (a.get("end") as? Long) ?: 0; return Result.success(Data.Builder().putString("result", Cwtch.getMessages(profile, handle, start, end)).build()) } + "UpdateMessageFlags" -> { + val profile = (a.get("profile") as? String) ?: ""; + val handle = (a.get("contact") as? String) ?: ""; + val midx = (a.get("midx") as? Long) ?: 0; + val flags = (a.get("flags") as? Long) ?: 0; + Cwtch.updateMessageFlags(profile, handle, midx, flags); + } "AcceptContact" -> { val profile = (a.get("ProfileOnion") as? String) ?: ""; val handle = (a.get("handle") as? String) ?: ""; From 28603b6fed0b4ad82973487bde26b789ab0320d5 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 14 Jun 2021 14:41:05 -0700 Subject: [PATCH 15/17] Remove debug button in release builds --- lib/main.dart | 34 ++++++++++++------------ lib/views/profilemgrview.dart | 49 +++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d2da036..25ad0f5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -112,23 +112,23 @@ class FlwtchState extends State { } Future _externalNotificationClicked(MethodCall call) async { - var args = jsonDecode(call.arguments); - var profile = profs.getProfile(args["ProfileOnion"])!; - var contact = profile.contactList.getContact(args["RemotePeer"])!; - contact.unreadMessages = 0; - navKey.currentState?.push( - MaterialPageRoute( - builder: (BuildContext builderContext) { - return MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: profile), - ChangeNotifierProvider.value(value: contact), - ], - builder: (context, child) => MessageView(), - ); - }, - ), - ); + var args = jsonDecode(call.arguments); + var profile = profs.getProfile(args["ProfileOnion"])!; + var contact = profile.contactList.getContact(args["RemotePeer"])!; + contact.unreadMessages = 0; + navKey.currentState?.push( + MaterialPageRoute( + builder: (BuildContext builderContext) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: profile), + ChangeNotifierProvider.value(value: contact), + ], + builder: (context, child) => MessageView(), + ); + }, + ), + ); } @override diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index e0f708f..1c8468b 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -9,9 +9,9 @@ import 'package:cwtch/widgets/tor_icon.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:cwtch/widgets/profilerow.dart'; import 'package:provider/provider.dart'; +import '../config.dart'; import '../main.dart'; import '../model.dart'; -import '../opaque.dart'; import '../torstatus.dart'; import 'addeditprofileview.dart'; import 'globalsettingsview.dart'; @@ -58,22 +58,7 @@ class _ProfileMgrViewState extends State { ), Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor()))) ]), - actions: [ - IconButton( - icon: TorIcon(), - onPressed: _pushTorStatus, - tooltip: Provider.of(context).progress == 100 - ? AppLocalizations.of(context)!.networkStatusOnline - : (Provider.of(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor), - ), - IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _setLoggingLevelDebug), - IconButton( - icon: Icon(CwtchIcons.lock_open_24px), - tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles, - onPressed: _modalUnlockProfiles, - ), - IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings), - ], + actions: getActions(), ), floatingActionButton: FloatingActionButton( onPressed: _pushAddEditProfile, @@ -88,6 +73,36 @@ class _ProfileMgrViewState extends State { ); } + List getActions() { + List actions = new List.empty(growable: true); + + // Tor Status + actions.add(IconButton( + icon: TorIcon(), + onPressed: _pushTorStatus, + tooltip: Provider.of(context).progress == 100 + ? AppLocalizations.of(context)!.networkStatusOnline + : (Provider.of(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor), + )); + + // Only show debug button on development builds + if (EnvironmentConfig.BUILD_VER == dev_version) { + actions.add(IconButton(icon: Icon(Icons.bug_report_outlined), tooltip: "Turn on Debug Logging", onPressed: _setLoggingLevelDebug)); + } + + // Unlock Profiles + actions.add(IconButton( + icon: Icon(CwtchIcons.lock_open_24px), + tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles, + onPressed: _modalUnlockProfiles, + )); + + // Global Settings + actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings)); + + return actions; + } + void _setLoggingLevelDebug() { final setLoggingLevel = { "EventType": "SetLoggingLevel", From c3e3c4f91edd9f08465a210c6eb058eb56548f1c Mon Sep 17 00:00:00 2001 From: erinn Date: Mon, 14 Jun 2021 17:11:43 -0700 Subject: [PATCH 16/17] l10n updates --- lib/l10n/intl_de.arb | 352 ++++++++++++++++++++++--------------------- lib/l10n/intl_en.arb | 352 ++++++++++++++++++++++--------------------- lib/l10n/intl_es.arb | 352 ++++++++++++++++++++++--------------------- lib/l10n/intl_fr.arb | 352 ++++++++++++++++++++++--------------------- lib/l10n/intl_it.arb | 352 ++++++++++++++++++++++--------------------- lib/l10n/intl_pt.arb | 352 ++++++++++++++++++++++--------------------- 6 files changed, 1104 insertions(+), 1008 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 89bb1d4..5c862eb 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,170 +1,186 @@ { - "@@locale": "de", - "accepted": "", - "acceptGroupBtn": "Annehmen", - "acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen", - "acknowledgedLabel": "bestätigt", - "addListItem": "Liste hinzufügen", - "addListItemBtn": "Element hinzufügen", - "addNewItem": "Ein neues Element zur Liste hinzufügen", - "addNewProfileBtn": "Neues Profil hinzufügen", - "addPeer": "Peer hinzufügen", - "addPeerTab": "Einen Peer hinzufügen", - "addProfileTitle": "Neues Profil hinzufügen", - "addressLabel": "Adresse", - "blockBtn": "Peer blockieren", - "blocked": "Blockiert", - "blockUnknownLabel": "Unbekannte Peers blockieren", - "builddate": "Aufgebaut auf: %2", - "bulletinsBtn": "Meldungen", - "chatBtn": "Chat", - "chatHistoryDefault": "", - "contactAlreadyExists": "", - "contactSuggestion": "", - "conversationSettings": "", - "copiedClipboardNotification": "in die Zwischenablage kopiert", - "copiedToClipboardNotification": "in die Zwischenablage kopiert", - "copyBtn": "Kopieren", - "couldNotSendMsgError": "Nachricht konnte nicht gesendet werden", - "createGroup": "Gruppe erstellen", - "createGroupBtn": "Anlegen", - "createGroupTab": "Eine Gruppe erstellen", - "createGroupTitle": "Gruppe Anlegen", - "createProfileBtn": "Profil speichern", - "currentPasswordLabel": "derzeitiges Passwort", - "cwtchSettingsTitle": "Cwtch Einstellungen", - "cycleCatsAndroid": "", - "cycleCatsDesktop": "", - "cycleColoursAndroid": "", - "cycleColoursDesktop": "", - "cycleMorphsAndroid": "", - "cycleMorphsDesktop": "", - "dateDaysAgo": "", - "dateHoursAgo": "", - "dateLastMonth": "", - "dateLastYear": "", - "dateMinutesAgo": "", - "dateMonthsAgo": "", - "dateNever": "", - "dateRightNow": "", - "dateWeeksAgo": "", - "dateYearsAgo": "", - "dateYesterday": "", - "defaultGroupName": "Tolle Gruppe", - "defaultProfileName": "Alice", - "defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:", - "deleteBtn": "Löschen", - "deleteConfirmLabel": "Geben Sie LÖSCHEN zur Bestätigung ein", - "deleteConfirmText": "LÖSCHEN", - "deleteProfileBtn": "Profil löschen", - "deleteProfileConfirmBtn": "Profil wirklich löschen", - "descriptionBlockUnknownConnections": "", - "descriptionExperiments": "", - "descriptionExperimentsGroups": "", - "displayNameLabel": "Angezeigter Name", - "dmTooltip": "Klicken, um DM zu senden", - "dontSavePeerHistory": "Peer-Verlauf löschen", - "editProfile": "Profil bearbeiten", - "editProfileTitle": "Profil bearbeiten", - "enableGroups": "", - "enterCurrentPasswordForDelete": "", - "enterProfilePassword": "Geben Sie ein Passwort ein, um Ihre Profile anzuzeigen", - "error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen", - "experimentsEnabled": "Experimente aktiviert", - "groupAddr": "Adresse", - "groupName": "Gruppenname", - "groupNameLabel": "Gruppenname", - "invalidImportString": "", - "invitation": "Einladung", - "invitationLabel": "Einladung", - "inviteBtn": "Einladen", - "inviteToGroup": "", - "inviteToGroupLabel": "In die Gruppe einladen", - "joinGroup": "Gruppe beitreten", - "joinGroupTab": "Einer Gruppe beitreten", - "largeTextLabel": "Groß", - "leaveGroup": "", - "listsBtn": "Listen", - "loadingTor": "Tor wird geladen...", - "localeDe": "Deutsche", - "localeEn": "", - "localeEs": "", - "localeFr": "", - "localeIt": "", - "localePt": "", - "membershipDescription": "Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben.", - "networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen", - "networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...", - "networkStatusDisconnected": "Vom Internet getrennt, überprüfen Sie Ihre Verbindung", - "networkStatusOnline": "Online", - "newBulletinLabel": "Neue Meldung", - "newConnectionPaneTitle": "Neue Verbindung", - "newGroupBtn": "Neue Gruppe anlegen", - "newPassword": "", - "newProfile": "Neues Profil", - "noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.", - "password": "Passwort", - "password1Label": "Passwort", - "password2Label": "Passwort erneut eingeben", - "passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt", - "passwordErrorEmpty": "Passwort kann nicht leer sein", - "passwordErrorMatch": "Passwörter stimmen nicht überein", - "pasteAddressToAddContact": "Adresse hier hinzufügen, um einen Kontakt aufzunehmen", - "peerAddress": "Adresse", - "peerBlockedMessage": "Peer ist blockiert", - "peerName": "Namen", - "peerNotOnline": "", - "peerOfflineMessage": "Peer ist offline, Nachrichten können derzeit nicht zugestellt werden", - "pendingLabel": "Bestätigung ausstehend", - "postNewBulletinLabel": "Neue Meldung veröffentlichen", - "profileName": "Anzeigename", - "profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten", - "puzzleGameBtn": "Puzzlespiel", - "radioNoPassword": "Unverschlüsselt (kein Passwort)", - "radioUsePassword": "Passwort", - "reallyLeaveThisGroupPrompt": "", - "rejected": "", - "rejectGroupBtn": "Ablehnen", - "saveBtn": "Speichern", - "savePeerHistory": "Peer-Verlauf speichern", - "savePeerHistoryDescription": "Legt fest, ob ein mit dem Peer verknüpfter Verlauf gelöscht werden soll oder nicht.", - "saveProfileBtn": "Profil speichern", - "search": "Suche...", - "searchList": "", - "sendAnInvitation": "", - "server": "Server", - "serverConnectivityConnected": "Server verbunden", - "serverConnectivityDisconnected": "Server getrennt", - "serverInfo": "Server-Informationen", - "serverLabel": "Server", - "serverNotSynced": "", - "serverSynced": "", - "settingInterfaceZoom": "Zoomstufe", - "settingLanguage": "Sprache", - "settingTheme": "Thema", - "smallTextLabel": "Klein", - "successfullAddedContact": "", - "themeDark": "Dunkel", - "themeLight": "Licht", - "titleManageContacts": "", - "titleManageProfiles": "", - "titleManageServers": "", - "titlePlaceholder": "Titel...", - "todoPlaceholder": "noch zu erledigen", - "tooltipAddContact": "", - "tooltipOpenSettings": "", - "tooltipUnlockProfiles": "", - "unblockBtn": "Peer entblockieren", - "unlock": "Entsperren", - "update": "", - "version": "Version %1", - "versionBuilddate": "Version: %1 Aufgebaut auf: %2", - "versionTor": "Version %1 mit tor %2", - "viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen", - "viewServerInfo": "", - "yesLeave": "", - "yourDisplayName": "Ihr Anzeigename", - "yourProfiles": "Ihre Profile", - "yourServers": "Ihre Server", - "zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)" + "@@locale": "de", + "@@last_modified": "2021-06-15T02:08:49+02:00", + "createGroupTitle": "Gruppe Anlegen", + "serverLabel": "Server", + "groupNameLabel": "Gruppenname", + "defaultGroupName": "Tolle Gruppe", + "createGroupBtn": "Anlegen", + "profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten", + "copyBtn": "Kopieren", + "copiedToClipboardNotification": "in die Zwischenablage kopiert", + "addPeerTab": "Einen Peer hinzufügen", + "createGroupTab": "Eine Gruppe erstellen", + "joinGroupTab": "Einer Gruppe beitreten", + "peerAddress": "Adresse", + "peerName": "Namen", + "groupName": "Gruppenname", + "server": "Server", + "invitation": "Einladung", + "groupAddr": "Adresse", + "addPeer": "Peer hinzufügen", + "createGroup": "Gruppe erstellen", + "joinGroup": "Gruppe beitreten", + "newBulletinLabel": "Neue Meldung", + "postNewBulletinLabel": "Neue Meldung veröffentlichen", + "titlePlaceholder": "Titel...", + "pasteAddressToAddContact": "Adresse hier hinzufügen, um einen Kontakt aufzunehmen", + "blocked": "Blockiert", + "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", + "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", + "cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.", + "cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.", + "cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.", + "cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.", + "search": "Suche...", + "invitationLabel": "Einladung", + "serverInfo": "Server-Informationen", + "serverConnectivityConnected": "Server verbunden", + "serverConnectivityDisconnected": "Server getrennt", + "serverSynced": "Synced", + "serverNotSynced": "Out of Sync", + "viewServerInfo": "Server Info", + "saveBtn": "speichern", + "inviteToGroupLabel": "In die Gruppe einladen", + "inviteBtn": "Einladen", + "deleteBtn": "löschen", + "update": "Update", + "searchList": "Search List", + "peerNotOnline": "Peer is Offline. Applications cannot be used right now.", + "addListItemBtn": "Element hinzufügen", + "membershipDescription": "Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben.", + "dmTooltip": "Klicken, um DM zu senden", + "couldNotSendMsgError": "Nachricht konnte nicht gesendet werden", + "acknowledgedLabel": "bestätigt", + "pendingLabel": "Bestätigung ausstehend", + "peerBlockedMessage": "Peer ist blockiert", + "peerOfflineMessage": "Peer ist offline, Nachrichten können derzeit nicht zugestellt werden", + "copiedClipboardNotification": "in die Zwischenablage kopiert", + "newGroupBtn": "Neue Gruppe anlegen", + "acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen", + "acceptGroupBtn": "Annehmen", + "rejectGroupBtn": "Ablehnen", + "chatBtn": "Chat", + "listsBtn": "Listen", + "bulletinsBtn": "Meldungen", + "puzzleGameBtn": "Puzzlespiel", + "addressLabel": "Adresse", + "displayNameLabel": "Angezeigter Name", + "blockBtn": "Peer blockieren", + "savePeerHistory": "Peer-Verlauf speichern", + "savePeerHistoryDescription": "Legt fest, ob ein mit dem Peer verknüpfter Verlauf gelöscht werden soll oder nicht.", + "dontSavePeerHistory": "Peer-Verlauf löschen", + "unblockBtn": "Peer entblockieren", + "addProfileTitle": "Neues Profil hinzufügen", + "editProfileTitle": "Profil bearbeiten", + "profileName": "Anzeigename", + "defaultProfileName": "Alice", + "newProfile": "Neues Profil", + "editProfile": "Profil bearbeiten", + "radioUsePassword": "Passwort", + "radioNoPassword": "Unverschlüsselt (kein Passwort)", + "noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.", + "yourDisplayName": "Ihr Anzeigename", + "currentPasswordLabel": "derzeitiges Passwort", + "password1Label": "Passwort", + "password2Label": "Passwort erneut eingeben", + "passwordErrorEmpty": "Passwort kann nicht leer sein", + "createProfileBtn": "Profil speichern", + "saveProfileBtn": "Profil speichern", + "passwordErrorMatch": "Passwörter stimmen nicht überein", + "passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt", + "deleteProfileBtn": "Profil löschen", + "deleteConfirmLabel": "Geben Sie LÖSCHEN zur Bestätigung ein", + "deleteProfileConfirmBtn": "Profil wirklich löschen", + "deleteConfirmText": "LÖSCHEN", + "addNewProfileBtn": "Neues Profil hinzufügen", + "enterProfilePassword": "Geben Sie ein Passwort ein, um Ihre Profile anzuzeigen", + "password": "Passwort", + "error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen", + "yourProfiles": "Ihre Profile", + "yourServers": "Ihre Server", + "unlock": "Entsperren", + "cwtchSettingsTitle": "Cwtch Einstellungen", + "versionBuilddate": "Version: %1 Aufgebaut auf: %2", + "zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)", + "blockUnknownLabel": "Unbekannte Peers blockieren", + "settingLanguage": "Sprache", + "localeEn": "English", + "localeFr": "Frances", + "localePt": "Portuguesa", + "localeDe": "Deutsche", + "settingInterfaceZoom": "Zoomstufe", + "largeTextLabel": "Groß", + "settingTheme": "Thema", + "themeLight": "Licht", + "themeDark": "Dunkel", + "experimentsEnabled": "Experimente aktiviert", + "versionTor": "Version %1 mit tor %2", + "version": "Version %1", + "builddate": "Aufgebaut auf: %2", + "defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:", + "smallTextLabel": "Klein", + "loadingTor": "Tor wird geladen...", + "viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen", + "networkStatusDisconnected": "Vom Internet getrennt, überprüfen Sie Ihre Verbindung", + "networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen", + "networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...", + "networkStatusOnline": "Online", + "newConnectionPaneTitle": "Neue Verbindung", + "addListItem": "Liste hinzufügen", + "addNewItem": "Ein neues Element zur Liste hinzufügen", + "todoPlaceholder": "noch zu erledigen", + "localeEs": "Espanol", + "localeIt": "Italiana", + "enableGroups": "Enable Group Chat", + "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", + "conversationSettings": "Conversation Settings", + "invalidImportString": "Invalid import string", + "contactAlreadyExists": "Contact Already Exists", + "tooltipOpenSettings": "Open the settings pane", + "tooltipAddContact": "Add a new contact or conversation", + "titleManageContacts": "Conversations", + "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageProfiles": "Manage Cwtch Profiles", + "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", + "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", + "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", + "successfullAddedContact": "Successfully added ", + "dateRightNow": "Right Now", + "dateMinutesAgo": "Minutes Ago", + "dateHoursAgo": "Hours Ago", + "dateDaysAgo": "Days Ago", + "dateWeeksAgo": "Weeks Ago", + "dateLastMonth": "Last Month", + "dateYesterday": "Yesterday", + "dateLastYear": "Last Year", + "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", + "dateNever": "Never", + "dateMonthsAgo": "Months Ago", + "titleManageServers": "Manage Servers", + "inviteToGroup": "You have been invited to join a group:", + "leaveGroup": "Leave This Conversation", + "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", + "yesLeave": "Yes, Leave This Conversation", + "newPassword": "New Password", + "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", + "accepted": "Accepted!", + "rejected": "Rejected!", + "contactSuggestion": "This is a contact suggestion for: ", + "sendAnInvitation": "You sent an invitation for: ", + "torVersion": "Tor Version", + "torStatus": "Tor Status", + "resetTor": "Reset", + "cancel": "Cancel", + "sendMessage": "Send Message", + "sendInvite": "Send a contact or group invite", + "deleteProfileSuccess": "Successfully deleted profile", + "addServerFirst": "You need to add a server before you can create a group", + "nickChangeSuccess": "Profile nickname changed successfully", + "createProfileToBegin": "Please create or unlock a profile to begin", + "addContactFirst": "Add or pick a contact to begin chatting.", + "torNetworkStatus": "Tor network status", + "debugLog": "Turn on console debug logging", + "profileDeleteSuccess": "Successfully deleted profile", + "malformedMessage": "Malformed message" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 35e3731..d4c68f5 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,170 +1,186 @@ { - "@@locale": "en", - "accepted": "Accepted!", - "acceptGroupBtn": "Accept", - "acceptGroupInviteLabel": "Do you want to accept the invitation to", - "acknowledgedLabel": "Acknowledged", - "addListItem": "Add a New List Item", - "addListItemBtn": "Add Item", - "addNewItem": "Add a new item to the list", - "addNewProfileBtn": "Add new profile", - "addPeer": "Add Peer", - "addPeerTab": "Add a peer", - "addProfileTitle": "Add new profile", - "addressLabel": "Address", - "blockBtn": "Block Peer", - "blocked": "Blocked", - "blockUnknownLabel": "Block Unknown Peers", - "builddate": "Built on: %2", - "bulletinsBtn": "Bulletins", - "chatBtn": "Chat", - "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", - "contactAlreadyExists": "Contact Already Exists", - "contactSuggestion": "This is a contact suggestion for: ", - "conversationSettings": "Conversation Settings", - "copiedClipboardNotification": "Copied to clipboard", - "copiedToClipboardNotification": "Copied to Clipboard", - "copyBtn": "Copy", - "couldNotSendMsgError": "Could not send this message", - "createGroup": "Create group", - "createGroupBtn": "Create", - "createGroupTab": "Create a group", - "createGroupTitle": "Create Group", - "createProfileBtn": "Create Profile", - "currentPasswordLabel": "Current Password", - "cwtchSettingsTitle": "Cwtch Settings", - "cycleCatsAndroid": "Click to cycle category.\\nLong-press to reset.", - "cycleCatsDesktop": "Click to cycle category.\\nRight-click to reset.", - "cycleColoursAndroid": "Click to cycle colours.\\nLong-press to reset.", - "cycleColoursDesktop": "Click to cycle colours.\\nRight-click to reset.", - "cycleMorphsAndroid": "Click to cycle morphs.\\nLong-press to reset.", - "cycleMorphsDesktop": "Click to cycle morphs.\\nRight-click to reset.", - "dateDaysAgo": "Days Ago", - "dateHoursAgo": "Hours Ago", - "dateLastMonth": "Last Month", - "dateLastYear": "Last Year", - "dateMinutesAgo": "Minutes Ago", - "dateMonthsAgo": "Months Ago", - "dateNever": "Never", - "dateRightNow": "Right Now", - "dateWeeksAgo": "Weeks Ago", - "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", - "dateYesterday": "Yesterday", - "defaultGroupName": "Awesome Group", - "defaultProfileName": "Alice", - "defaultScalingText": "Default size text (scale factor:", - "deleteBtn": "Delete", - "deleteConfirmLabel": "Type DELETE to confirm", - "deleteConfirmText": "DELETE", - "deleteProfileBtn": "Delete Profile", - "deleteProfileConfirmBtn": "Really Delete Profile", - "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", - "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", - "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", - "displayNameLabel": "Display Name", - "dmTooltip": "Click to DM", - "dontSavePeerHistory": "Delete Peer History", - "editProfile": "Edit Profille", - "editProfileTitle": "Edit Profile", - "enableGroups": "Enable Group Chat", - "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", - "enterProfilePassword": "Enter a password to view your profiles", - "error0ProfilesLoadedForPassword": "0 profiles loaded with that password", - "experimentsEnabled": "Enable Experiments", - "groupAddr": "Address", - "groupName": "Group name", - "groupNameLabel": "Group Name", - "invalidImportString": "Invalid import string", - "invitation": "Invitation", - "invitationLabel": "Invitation", - "inviteBtn": "Invite", - "inviteToGroup": "You have been invited to join a group:", - "inviteToGroupLabel": "Invite to group", - "joinGroup": "Join group", - "joinGroupTab": "Join a group", - "largeTextLabel": "Large", - "leaveGroup": "Leave This Conversation", - "listsBtn": "Lists", - "loadingTor": "Loading tor...", - "localeDe": "Deutsche", - "localeEn": "English", - "localeEs": "Espanol", - "localeFr": "Frances", - "localeIt": "Italiana", - "localePt": "Portuguesa", - "membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.", - "networkStatusAttemptingTor": "Attempting to connect to Tor network", - "networkStatusConnecting": "Connecting to network and peers...", - "networkStatusDisconnected": "Disconnected from the internet, check your connection", - "networkStatusOnline": "Online", - "newBulletinLabel": "New Bulletin", - "newConnectionPaneTitle": "New Connection", - "newGroupBtn": "Create new group", - "newPassword": "New Password", - "newProfile": "New Profile", - "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", - "password": "Password", - "password1Label": "Password", - "password2Label": "Reenter password", - "passwordChangeError": "Error changing password: Supplied password rejected", - "passwordErrorEmpty": "Password cannot be empty", - "passwordErrorMatch": "Passwords do not match", - "pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation", - "peerAddress": "Address", - "peerBlockedMessage": "Peer is blocked", - "peerName": "Name", - "peerNotOnline": "Peer is Offline. Applications cannot be used right now.", - "peerOfflineMessage": "Peer is offline, messages can't be delivered right now", - "pendingLabel": "Pending", - "postNewBulletinLabel": "Post new bulletin", - "profileName": "Display name", - "profileOnionLabel": "Send this address to peers you want to connect with", - "puzzleGameBtn": "Puzzle Game", - "radioNoPassword": "Unencrypted (No password)", - "radioUsePassword": "Password", - "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", - "rejected": "Rejected!", - "rejectGroupBtn": "Reject", - "saveBtn": "Save", - "savePeerHistory": "Save Peer History", - "savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.", - "saveProfileBtn": "Save Profile", - "search": "Search...", - "searchList": "Search List", - "sendAnInvitation": "You sent an invitation for: ", - "server": "Server", - "serverConnectivityConnected": "Server Connected", - "serverConnectivityDisconnected": "Server Disconnected", - "serverInfo": "Server Information", - "serverLabel": "Server", - "serverNotSynced": "Out of Sync", - "serverSynced": "Synced", - "settingInterfaceZoom": "Zoom level", - "settingLanguage": "Language", - "settingTheme": "Theme", - "smallTextLabel": "Small", - "successfullAddedContact": "Successfully added ", - "themeDark": "Dark", - "themeLight": "Light", - "titleManageContacts": "Conversations", - "titleManageProfiles": "Manage Cwtch Profiles", - "titleManageServers": "Manage Servers", - "titlePlaceholder": "title...", - "todoPlaceholder": "Todo...", - "tooltipAddContact": "Add a new contact or conversation", - "tooltipOpenSettings": "Open the settings pane", - "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", - "unblockBtn": "Unblock Peer", - "unlock": "Unlock", - "update": "Update", - "version": "Version %1", - "versionBuilddate": "Version: %1 Built on: %2", - "versionTor": "Version %1 with tor %2", - "viewGroupMembershipTooltip": "View Group Membership", - "viewServerInfo": "Server Info", - "yesLeave": "Yes, Leave This Conversation", - "yourDisplayName": "Your Display Name", - "yourProfiles": "Your Profiles", - "yourServers": "Your Servers", - "zoomLabel": "Interface zoom (mostly affects text and button sizes)" + "@@locale": "en", + "@@last_modified": "2021-06-15T02:08:49+02:00", + "createGroupTitle": "Create Group", + "serverLabel": "Server", + "groupNameLabel": "Group Name", + "defaultGroupName": "Awesome Group", + "createGroupBtn": "Create", + "profileOnionLabel": "Send this address to peers you want to connect with", + "copyBtn": "Copy", + "copiedToClipboardNotification": "Copied to Clipboard", + "addPeerTab": "Add a peer", + "createGroupTab": "Create a group", + "joinGroupTab": "Join a group", + "peerAddress": "Address", + "peerName": "Name", + "groupName": "Group name", + "server": "Server", + "invitation": "Invitation", + "groupAddr": "Address", + "addPeer": "Add Peer", + "createGroup": "Create group", + "joinGroup": "Join group", + "newBulletinLabel": "New Bulletin", + "postNewBulletinLabel": "Post new bulletin", + "titlePlaceholder": "title...", + "pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation", + "blocked": "Blocked", + "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", + "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", + "cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.", + "cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.", + "cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.", + "cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.", + "search": "Search...", + "invitationLabel": "Invitation", + "serverInfo": "Server Information", + "serverConnectivityConnected": "Server Connected", + "serverConnectivityDisconnected": "Server Disconnected", + "serverSynced": "Synced", + "serverNotSynced": "Out of Sync", + "viewServerInfo": "Server Info", + "saveBtn": "Save", + "inviteToGroupLabel": "Invite to group", + "inviteBtn": "Invite", + "deleteBtn": "Delete", + "update": "Update", + "searchList": "Search List", + "peerNotOnline": "Peer is Offline. Applications cannot be used right now.", + "addListItemBtn": "Add Item", + "membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.", + "dmTooltip": "Click to DM", + "couldNotSendMsgError": "Could not send this message", + "acknowledgedLabel": "Acknowledged", + "pendingLabel": "Pending", + "peerBlockedMessage": "Peer is blocked", + "peerOfflineMessage": "Peer is offline, messages can't be delivered right now", + "copiedClipboardNotification": "Copied to clipboard", + "newGroupBtn": "Create new group", + "acceptGroupInviteLabel": "Do you want to accept the invitation to", + "acceptGroupBtn": "Accept", + "rejectGroupBtn": "Reject", + "chatBtn": "Chat", + "listsBtn": "Lists", + "bulletinsBtn": "Bulletins", + "puzzleGameBtn": "Puzzle Game", + "addressLabel": "Address", + "displayNameLabel": "Display Name", + "blockBtn": "Block Peer", + "savePeerHistory": "Save Peer History", + "savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.", + "dontSavePeerHistory": "Delete Peer History", + "unblockBtn": "Unblock Peer", + "addProfileTitle": "Add new profile", + "editProfileTitle": "Edit Profile", + "profileName": "Display name", + "defaultProfileName": "Alice", + "newProfile": "New Profile", + "editProfile": "Edit Profille", + "radioUsePassword": "Password", + "radioNoPassword": "Unencrypted (No password)", + "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", + "yourDisplayName": "Your Display Name", + "currentPasswordLabel": "Current Password", + "password1Label": "Password", + "password2Label": "Reenter password", + "passwordErrorEmpty": "Password cannot be empty", + "createProfileBtn": "Create Profile", + "saveProfileBtn": "Save Profile", + "passwordErrorMatch": "Passwords do not match", + "passwordChangeError": "Error changing password: Supplied password rejected", + "deleteProfileBtn": "Delete Profile", + "deleteConfirmLabel": "Type DELETE to confirm", + "deleteProfileConfirmBtn": "Really Delete Profile", + "deleteConfirmText": "DELETE", + "addNewProfileBtn": "Add new profile", + "enterProfilePassword": "Enter a password to view your profiles", + "password": "Password", + "error0ProfilesLoadedForPassword": "0 profiles loaded with that password", + "yourProfiles": "Your Profiles", + "yourServers": "Your Servers", + "unlock": "Unlock", + "cwtchSettingsTitle": "Cwtch Settings", + "versionBuilddate": "Version: %1 Built on: %2", + "zoomLabel": "Interface zoom (mostly affects text and button sizes)", + "blockUnknownLabel": "Block Unknown Peers", + "settingLanguage": "Language", + "localeEn": "English", + "localeFr": "Frances", + "localePt": "Portuguesa", + "localeDe": "Deutsche", + "settingInterfaceZoom": "Zoom level", + "largeTextLabel": "Large", + "settingTheme": "Theme", + "themeLight": "Light", + "themeDark": "Dark", + "experimentsEnabled": "Enable Experiments", + "versionTor": "Version %1 with tor %2", + "version": "Version %1", + "builddate": "Built on: %2", + "defaultScalingText": "Default size text (scale factor:", + "smallTextLabel": "Small", + "loadingTor": "Loading tor...", + "viewGroupMembershipTooltip": "View Group Membership", + "networkStatusDisconnected": "Disconnected from the internet, check your connection", + "networkStatusAttemptingTor": "Attempting to connect to Tor network", + "networkStatusConnecting": "Connecting to network and peers...", + "networkStatusOnline": "Online", + "newConnectionPaneTitle": "New Connection", + "addListItem": "Add a New List Item", + "addNewItem": "Add a new item to the list", + "todoPlaceholder": "Todo...", + "localeEs": "Espanol", + "localeIt": "Italiana", + "enableGroups": "Enable Group Chat", + "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", + "conversationSettings": "Conversation Settings", + "invalidImportString": "Invalid import string", + "contactAlreadyExists": "Contact Already Exists", + "tooltipOpenSettings": "Open the settings pane", + "tooltipAddContact": "Add a new contact or conversation", + "titleManageContacts": "Conversations", + "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageProfiles": "Manage Cwtch Profiles", + "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", + "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", + "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", + "successfullAddedContact": "Successfully added ", + "dateRightNow": "Right Now", + "dateMinutesAgo": "Minutes Ago", + "dateHoursAgo": "Hours Ago", + "dateDaysAgo": "Days Ago", + "dateWeeksAgo": "Weeks Ago", + "dateLastMonth": "Last Month", + "dateYesterday": "Yesterday", + "dateLastYear": "Last Year", + "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", + "dateNever": "Never", + "dateMonthsAgo": "Months Ago", + "titleManageServers": "Manage Servers", + "inviteToGroup": "You have been invited to join a group:", + "leaveGroup": "Leave This Conversation", + "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", + "yesLeave": "Yes, Leave This Conversation", + "newPassword": "New Password", + "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", + "accepted": "Accepted!", + "rejected": "Rejected!", + "contactSuggestion": "This is a contact suggestion for: ", + "sendAnInvitation": "You sent an invitation for: ", + "torVersion": "Tor Version", + "torStatus": "Tor Status", + "resetTor": "Reset", + "cancel": "Cancel", + "sendMessage": "Send Message", + "sendInvite": "Send a contact or group invite", + "deleteProfileSuccess": "Successfully deleted profile", + "addServerFirst": "You need to add a server before you can create a group", + "nickChangeSuccess": "Profile nickname changed successfully", + "createProfileToBegin": "Please create or unlock a profile to begin", + "addContactFirst": "Add or pick a contact to begin chatting.", + "torNetworkStatus": "Tor network status", + "debugLog": "Turn on console debug logging", + "profileDeleteSuccess": "Successfully deleted profile", + "malformedMessage": "Malformed message" } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 7e0ad97..c48b4ae 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,170 +1,186 @@ { - "@@locale": "es", - "accepted": "", - "acceptGroupBtn": "Aceptar", - "acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ", - "acknowledgedLabel": "Reconocido", - "addListItem": "Añadir un nuevo elemento a la lista", - "addListItemBtn": "Agregar artículo", - "addNewItem": "Añadir un nuevo elemento a la lista", - "addNewProfileBtn": "Agregar nuevo perfil", - "addPeer": "Agregar Contacto", - "addPeerTab": "Agregar Contacto", - "addProfileTitle": "Agregar nuevo perfil", - "addressLabel": "Dirección", - "blockBtn": "Bloquear contacto", - "blocked": "Bloqueado", - "blockUnknownLabel": "Bloquear conexiones desconocidas", - "builddate": "Basado en: %2", - "bulletinsBtn": "Boletines", - "chatBtn": "Chat", - "chatHistoryDefault": "", - "contactAlreadyExists": "", - "contactSuggestion": "", - "conversationSettings": "", - "copiedClipboardNotification": "Copiado al portapapeles", - "copiedToClipboardNotification": "Copiado al portapapeles", - "copyBtn": "Copiar", - "couldNotSendMsgError": "No se pudo enviar este mensaje", - "createGroup": "Crear perfil", - "createGroupBtn": "Crear", - "createGroupTab": "Crear un grupo", - "createGroupTitle": "Crear un grupo", - "createProfileBtn": "Crear perfil", - "currentPasswordLabel": "Contraseña actual", - "cwtchSettingsTitle": "Configuración de Cwtch", - "cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.", - "cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.", - "cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.", - "cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.", - "cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.", - "cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.", - "dateDaysAgo": "", - "dateHoursAgo": "", - "dateLastMonth": "", - "dateLastYear": "", - "dateMinutesAgo": "", - "dateMonthsAgo": "", - "dateNever": "", - "dateRightNow": "", - "dateWeeksAgo": "", - "dateYearsAgo": "", - "dateYesterday": "", - "defaultGroupName": "El Grupo Asombroso", - "defaultProfileName": "Alicia", - "defaultScalingText": "Tamaño predeterminado de texto (factor de escala:", - "deleteBtn": "Eliminar", - "deleteConfirmLabel": "Escribe ELIMINAR para confirmar", - "deleteConfirmText": "ELIMINAR", - "deleteProfileBtn": "Eliminar Perfil", - "deleteProfileConfirmBtn": "Confirmar eliminar perfil", - "descriptionBlockUnknownConnections": "", - "descriptionExperiments": "", - "descriptionExperimentsGroups": "", - "displayNameLabel": "Nombre de Usuario", - "dmTooltip": "Haz clic para enviar mensaje directo", - "dontSavePeerHistory": "Eliminar historial de contacto", - "editProfile": "Editar perfil", - "editProfileTitle": "Editar perfil", - "enableGroups": "", - "enterCurrentPasswordForDelete": "", - "enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles", - "error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña", - "experimentsEnabled": "Experimentos habilitados", - "groupAddr": "Dirección", - "groupName": "Nombre del grupo", - "groupNameLabel": "Nombre del grupo", - "invalidImportString": "", - "invitation": "Invitación", - "invitationLabel": "Invitación", - "inviteBtn": "Invitar", - "inviteToGroup": "", - "inviteToGroupLabel": "Invitar al grupo", - "joinGroup": "Únete al grupo", - "joinGroupTab": "Únete a un grupo", - "largeTextLabel": "Grande", - "leaveGroup": "", - "listsBtn": "Listas", - "loadingTor": "Cargando tor...", - "localeDe": "Alemán", - "localeEn": "Inglés", - "localeEs": "Español", - "localeFr": "Francés", - "localeIt": "Italiano", - "localePt": "Portugués", - "membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo", - "networkStatusAttemptingTor": "Intentando conectarse a la red Tor", - "networkStatusConnecting": "Conectando a la red y a los contactos...", - "networkStatusDisconnected": "Sin conexión, comprueba tu conexión", - "networkStatusOnline": "En línea", - "newBulletinLabel": "Nuevo Boletín", - "newConnectionPaneTitle": "Nueva conexión", - "newGroupBtn": "Crear un nuevo grupo de chat", - "newPassword": "", - "newProfile": "Nuevo perfil", - "noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados", - "password": "Contraseña", - "password1Label": "Contraseña", - "password2Label": "Vuelve a ingresar tu contraseña", - "passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada", - "passwordErrorEmpty": "El campo de contraseña no puede estar vacío", - "passwordErrorMatch": "Las contraseñas no coinciden", - "pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...", - "peerAddress": "Dirección", - "peerBlockedMessage": "Contacto bloqueado", - "peerName": "Nombre", - "peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento", - "peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento", - "pendingLabel": "Pendiente", - "postNewBulletinLabel": "Publicar nuevo boletín", - "profileName": "Nombre de Usuario", - "profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte", - "puzzleGameBtn": "Juego de rompecabezas", - "radioNoPassword": "Sin cifrado (sin contraseña)", - "radioUsePassword": "Contraseña", - "reallyLeaveThisGroupPrompt": "", - "rejected": "", - "rejectGroupBtn": "Rechazar", - "saveBtn": "Guardar", - "savePeerHistory": "Guardar el historial con contacto", - "savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.", - "saveProfileBtn": "Guardar perfil", - "search": "Búsqueda...", - "searchList": "Buscar en la lista", - "sendAnInvitation": "", - "server": "Servidor", - "serverConnectivityConnected": "Servidor conectado", - "serverConnectivityDisconnected": "Servidor desconectado", - "serverInfo": "Información del servidor", - "serverLabel": "Servidor", - "serverNotSynced": "Fuera de sincronización con el servidor", - "serverSynced": "Sincronizado", - "settingInterfaceZoom": "Nivel de zoom", - "settingLanguage": "Idioma", - "settingTheme": "Tema", - "smallTextLabel": "Pequeño", - "successfullAddedContact": "", - "themeDark": "Oscuro", - "themeLight": "Claro", - "titleManageContacts": "", - "titleManageProfiles": "", - "titleManageServers": "", - "titlePlaceholder": "título...", - "todoPlaceholder": "Por hacer...", - "tooltipAddContact": "", - "tooltipOpenSettings": "", - "tooltipUnlockProfiles": "", - "unblockBtn": "Desbloquear contacto", - "unlock": "Desbloquear", - "update": "Actualizar", - "version": "Versión %1", - "versionBuilddate": "Versión: %1 Basado en %2", - "versionTor": "Versión %1 con tor %2", - "viewGroupMembershipTooltip": "Ver membresía del grupo", - "viewServerInfo": "Información del servidor", - "yesLeave": "", - "yourDisplayName": "Tu nombre de usuario", - "yourProfiles": "Tus perfiles", - "yourServers": "Tus servidores", - "zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)" + "@@locale": "es", + "@@last_modified": "2021-06-15T02:08:49+02:00", + "createGroupTitle": "Crear un grupo", + "serverLabel": "Servidor", + "groupNameLabel": "Nombre del grupo", + "defaultGroupName": "El Grupo Asombroso", + "createGroupBtn": "Crear", + "profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte", + "copyBtn": "Copiar", + "copiedToClipboardNotification": "Copiado al portapapeles", + "addPeerTab": "Agregar Contacto", + "createGroupTab": "Crear un grupo", + "joinGroupTab": "Únete a un grupo", + "peerAddress": "Dirección", + "peerName": "Nombre", + "groupName": "Nombre del grupo", + "server": "Servidor", + "invitation": "Invitación", + "groupAddr": "Dirección", + "addPeer": "Agregar Contacto", + "createGroup": "Crear perfil", + "joinGroup": "Únete al grupo", + "newBulletinLabel": "Nuevo Boletín", + "postNewBulletinLabel": "Publicar nuevo boletín", + "titlePlaceholder": "título...", + "pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...", + "blocked": "Bloqueado", + "cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.", + "cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.", + "cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.", + "cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.", + "cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.", + "cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.", + "search": "Búsqueda...", + "invitationLabel": "Invitación", + "serverInfo": "Información del servidor", + "serverConnectivityConnected": "Servidor conectado", + "serverConnectivityDisconnected": "Servidor desconectado", + "serverSynced": "Sincronizado", + "serverNotSynced": "Fuera de sincronización con el servidor", + "viewServerInfo": "Información del servidor", + "saveBtn": "Guardar", + "inviteToGroupLabel": "Invitar al grupo", + "inviteBtn": "Invitar", + "deleteBtn": "Eliminar", + "update": "Actualizar", + "searchList": "Buscar en la lista", + "peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento", + "addListItemBtn": "Agregar artículo", + "membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo", + "dmTooltip": "Haz clic para enviar mensaje directo", + "couldNotSendMsgError": "No se pudo enviar este mensaje", + "acknowledgedLabel": "Reconocido", + "pendingLabel": "Pendiente", + "peerBlockedMessage": "Contacto bloqueado", + "peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento", + "copiedClipboardNotification": "Copiado al portapapeles", + "newGroupBtn": "Crear un nuevo grupo de chat", + "acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ", + "acceptGroupBtn": "Aceptar", + "rejectGroupBtn": "Rechazar", + "chatBtn": "Chat", + "listsBtn": "Listas", + "bulletinsBtn": "Boletines", + "puzzleGameBtn": "Juego de rompecabezas", + "addressLabel": "Dirección", + "displayNameLabel": "Nombre de Usuario", + "blockBtn": "Bloquear contacto", + "savePeerHistory": "Guardar el historial con contacto", + "savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.", + "dontSavePeerHistory": "Eliminar historial de contacto", + "unblockBtn": "Desbloquear contacto", + "addProfileTitle": "Agregar nuevo perfil", + "editProfileTitle": "Editar perfil", + "profileName": "Nombre de Usuario", + "defaultProfileName": "Alicia", + "newProfile": "Nuevo perfil", + "editProfile": "Editar perfil", + "radioUsePassword": "Contraseña", + "radioNoPassword": "Sin cifrado (sin contraseña)", + "noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados", + "yourDisplayName": "Tu nombre de usuario", + "currentPasswordLabel": "Contraseña actual", + "password1Label": "Contraseña", + "password2Label": "Vuelve a ingresar tu contraseña", + "passwordErrorEmpty": "El campo de contraseña no puede estar vacío", + "createProfileBtn": "Crear perfil", + "saveProfileBtn": "Guardar perfil", + "passwordErrorMatch": "Las contraseñas no coinciden", + "passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada", + "deleteProfileBtn": "Eliminar Perfil", + "deleteConfirmLabel": "Escribe ELIMINAR para confirmar", + "deleteProfileConfirmBtn": "Confirmar eliminar perfil", + "deleteConfirmText": "ELIMINAR", + "addNewProfileBtn": "Agregar nuevo perfil", + "enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles", + "password": "Contraseña", + "error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña", + "yourProfiles": "Tus perfiles", + "yourServers": "Tus servidores", + "unlock": "Desbloquear", + "cwtchSettingsTitle": "Configuración de Cwtch", + "versionBuilddate": "Versión: %1 Basado en %2", + "zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)", + "blockUnknownLabel": "Bloquear conexiones desconocidas", + "settingLanguage": "Idioma", + "localeEn": "Inglés", + "localeFr": "Francés", + "localePt": "Portugués", + "localeDe": "Alemán", + "settingInterfaceZoom": "Nivel de zoom", + "largeTextLabel": "Grande", + "settingTheme": "Tema", + "themeLight": "Claro", + "themeDark": "Oscuro", + "experimentsEnabled": "Experimentos habilitados", + "versionTor": "Versión %1 con tor %2", + "version": "Versión %1", + "builddate": "Basado en: %2", + "defaultScalingText": "Tamaño predeterminado de texto (factor de escala:", + "smallTextLabel": "Pequeño", + "loadingTor": "Cargando tor...", + "viewGroupMembershipTooltip": "Ver membresía del grupo", + "networkStatusDisconnected": "Sin conexión, comprueba tu conexión", + "networkStatusAttemptingTor": "Intentando conectarse a la red Tor", + "networkStatusConnecting": "Conectando a la red y a los contactos...", + "networkStatusOnline": "En línea", + "newConnectionPaneTitle": "Nueva conexión", + "addListItem": "Añadir un nuevo elemento a la lista", + "addNewItem": "Añadir un nuevo elemento a la lista", + "todoPlaceholder": "Por hacer...", + "localeEs": "Español", + "localeIt": "Italiano", + "enableGroups": "Enable Group Chat", + "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", + "conversationSettings": "Conversation Settings", + "invalidImportString": "Invalid import string", + "contactAlreadyExists": "Contact Already Exists", + "tooltipOpenSettings": "Open the settings pane", + "tooltipAddContact": "Add a new contact or conversation", + "titleManageContacts": "Conversations", + "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageProfiles": "Manage Cwtch Profiles", + "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", + "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", + "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", + "successfullAddedContact": "Successfully added ", + "dateRightNow": "Right Now", + "dateMinutesAgo": "Minutes Ago", + "dateHoursAgo": "Hours Ago", + "dateDaysAgo": "Days Ago", + "dateWeeksAgo": "Weeks Ago", + "dateLastMonth": "Last Month", + "dateYesterday": "Yesterday", + "dateLastYear": "Last Year", + "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", + "dateNever": "Never", + "dateMonthsAgo": "Months Ago", + "titleManageServers": "Manage Servers", + "inviteToGroup": "You have been invited to join a group:", + "leaveGroup": "Leave This Conversation", + "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", + "yesLeave": "Yes, Leave This Conversation", + "newPassword": "New Password", + "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", + "accepted": "Accepted!", + "rejected": "Rejected!", + "contactSuggestion": "This is a contact suggestion for: ", + "sendAnInvitation": "You sent an invitation for: ", + "torVersion": "Tor Version", + "torStatus": "Tor Status", + "resetTor": "Reset", + "cancel": "Cancel", + "sendMessage": "Send Message", + "sendInvite": "Send a contact or group invite", + "deleteProfileSuccess": "Successfully deleted profile", + "addServerFirst": "You need to add a server before you can create a group", + "nickChangeSuccess": "Profile nickname changed successfully", + "createProfileToBegin": "Please create or unlock a profile to begin", + "addContactFirst": "Add or pick a contact to begin chatting.", + "torNetworkStatus": "Tor network status", + "debugLog": "Turn on console debug logging", + "profileDeleteSuccess": "Successfully deleted profile", + "malformedMessage": "Malformed message" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 5e6c18f..5dba514 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,170 +1,186 @@ { - "@@locale": "fr", - "accepted": "", - "acceptGroupBtn": "Accepter", - "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe", - "acknowledgedLabel": "Confirmé", - "addListItem": "Ajouter un nouvel élément", - "addListItemBtn": "", - "addNewItem": "Ajouter un nouvel élément à la liste", - "addNewProfileBtn": "", - "addPeer": "", - "addPeerTab": "", - "addProfileTitle": "", - "addressLabel": "Adresse", - "blockBtn": "", - "blocked": "", - "blockUnknownLabel": "", - "builddate": "", - "bulletinsBtn": "Bulletins", - "chatBtn": "Discuter", - "chatHistoryDefault": "", - "contactAlreadyExists": "", - "contactSuggestion": "", - "conversationSettings": "", - "copiedClipboardNotification": "Copié dans le presse-papier", - "copiedToClipboardNotification": "Copié dans le presse-papier", - "copyBtn": "Copier", - "couldNotSendMsgError": "Impossible d'envoyer ce message", - "createGroup": "", - "createGroupBtn": "Créer", - "createGroupTab": "", - "createGroupTitle": "Créer un groupe", - "createProfileBtn": "", - "currentPasswordLabel": "", - "cwtchSettingsTitle": "Préférences Cwtch", - "cycleCatsAndroid": "", - "cycleCatsDesktop": "", - "cycleColoursAndroid": "", - "cycleColoursDesktop": "", - "cycleMorphsAndroid": "", - "cycleMorphsDesktop": "", - "dateDaysAgo": "", - "dateHoursAgo": "", - "dateLastMonth": "", - "dateLastYear": "", - "dateMinutesAgo": "", - "dateMonthsAgo": "", - "dateNever": "", - "dateRightNow": "", - "dateWeeksAgo": "", - "dateYearsAgo": "", - "dateYesterday": "", - "defaultGroupName": "Un super groupe", - "defaultProfileName": "", - "defaultScalingText": "Taille par défaut du texte (échelle:", - "deleteBtn": "Effacer", - "deleteConfirmLabel": "", - "deleteConfirmText": "", - "deleteProfileBtn": "", - "deleteProfileConfirmBtn": "", - "descriptionBlockUnknownConnections": "", - "descriptionExperiments": "", - "descriptionExperimentsGroups": "", - "displayNameLabel": "Pseudo", - "dmTooltip": "Envoyer un message privé", - "dontSavePeerHistory": "", - "editProfile": "", - "editProfileTitle": "", - "enableGroups": "", - "enterCurrentPasswordForDelete": "", - "enterProfilePassword": "", - "error0ProfilesLoadedForPassword": "", - "experimentsEnabled": "", - "groupAddr": "", - "groupName": "", - "groupNameLabel": "Nom du groupe", - "invalidImportString": "", - "invitation": "", - "invitationLabel": "Invitation", - "inviteBtn": "Invitation", - "inviteToGroup": "", - "inviteToGroupLabel": "Inviter quelqu'un", - "joinGroup": "", - "joinGroupTab": "", - "largeTextLabel": "Large", - "leaveGroup": "", - "listsBtn": "Listes", - "loadingTor": "", - "localeDe": "", - "localeEn": "", - "localeEs": "", - "localeFr": "", - "localeIt": "", - "localePt": "", - "membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe.", - "networkStatusAttemptingTor": "", - "networkStatusConnecting": "", - "networkStatusDisconnected": "", - "networkStatusOnline": "", - "newBulletinLabel": "Nouveau bulletin", - "newConnectionPaneTitle": "", - "newGroupBtn": "Créer un nouveau groupe", - "newPassword": "", - "newProfile": "", - "noPasswordWarning": "", - "password": "", - "password1Label": "", - "password2Label": "", - "passwordChangeError": "", - "passwordErrorEmpty": "", - "passwordErrorMatch": "", - "pasteAddressToAddContact": "... coller une adresse ici pour ajouter un contact...", - "peerAddress": "", - "peerBlockedMessage": "", - "peerName": "", - "peerNotOnline": "", - "peerOfflineMessage": "", - "pendingLabel": "En attente", - "postNewBulletinLabel": "Envoyer un nouveau bulletin", - "profileName": "", - "profileOnionLabel": "", - "puzzleGameBtn": "Puzzle", - "radioNoPassword": "", - "radioUsePassword": "", - "reallyLeaveThisGroupPrompt": "", - "rejected": "", - "rejectGroupBtn": "Refuser", - "saveBtn": "Sauvegarder", - "savePeerHistory": "", - "savePeerHistoryDescription": "", - "saveProfileBtn": "", - "search": "", - "searchList": "", - "sendAnInvitation": "", - "server": "", - "serverConnectivityConnected": "", - "serverConnectivityDisconnected": "", - "serverInfo": "", - "serverLabel": "Serveur", - "serverNotSynced": "", - "serverSynced": "", - "settingInterfaceZoom": "", - "settingLanguage": "", - "settingTheme": "", - "smallTextLabel": "Petit", - "successfullAddedContact": "", - "themeDark": "", - "themeLight": "", - "titleManageContacts": "", - "titleManageProfiles": "", - "titleManageServers": "", - "titlePlaceholder": "titre...", - "todoPlaceholder": "A faire...", - "tooltipAddContact": "", - "tooltipOpenSettings": "", - "tooltipUnlockProfiles": "", - "unblockBtn": "", - "unlock": "", - "update": "", - "version": "", - "versionBuilddate": "", - "versionTor": "", - "viewGroupMembershipTooltip": "", - "viewServerInfo": "", - "yesLeave": "", - "yourDisplayName": "", - "yourProfiles": "", - "yourServers": "", - "zoomLabel": "Interface zoom (essentiellement la taille du texte et des composants de l'interface)" + "@@locale": "fr", + "@@last_modified": "2021-06-15T02:08:49+02:00", + "createGroupTitle": "Créer un groupe", + "serverLabel": "Serveur", + "groupNameLabel": "Nom du groupe", + "defaultGroupName": "Un super groupe", + "createGroupBtn": "Créer", + "profileOnionLabel": "Send this address to peers you want to connect with", + "copyBtn": "Copier", + "copiedToClipboardNotification": "Copié dans le presse-papier", + "addPeerTab": "Add a peer", + "createGroupTab": "Create a group", + "joinGroupTab": "Join a group", + "peerAddress": "Address", + "peerName": "Name", + "groupName": "Group name", + "server": "Server", + "invitation": "Invitation", + "groupAddr": "Address", + "addPeer": "Add Peer", + "createGroup": "Create group", + "joinGroup": "Join group", + "newBulletinLabel": "Nouveau bulletin", + "postNewBulletinLabel": "Envoyer un nouveau bulletin", + "titlePlaceholder": "titre...", + "pasteAddressToAddContact": "... coller une adresse ici pour ajouter un contact...", + "blocked": "Blocked", + "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", + "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", + "cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.", + "cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.", + "cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.", + "cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.", + "search": "Search...", + "invitationLabel": "Invitation", + "serverInfo": "Server Information", + "serverConnectivityConnected": "Server Connected", + "serverConnectivityDisconnected": "Server Disconnected", + "serverSynced": "Synced", + "serverNotSynced": "Out of Sync", + "viewServerInfo": "Server Info", + "saveBtn": "Sauvegarder", + "inviteToGroupLabel": "Inviter quelqu'un", + "inviteBtn": "Invitation", + "deleteBtn": "Effacer", + "update": "Update", + "searchList": "Search List", + "peerNotOnline": "Peer is Offline. Applications cannot be used right now.", + "addListItemBtn": "Add Item", + "membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe.", + "dmTooltip": "Envoyer un message privé", + "couldNotSendMsgError": "Impossible d'envoyer ce message", + "acknowledgedLabel": "Confirmé", + "pendingLabel": "En attente", + "peerBlockedMessage": "Peer is blocked", + "peerOfflineMessage": "Peer is offline, messages can't be delivered right now", + "copiedClipboardNotification": "Copié dans le presse-papier", + "newGroupBtn": "Créer un nouveau groupe", + "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe", + "acceptGroupBtn": "Accepter", + "rejectGroupBtn": "Refuser", + "chatBtn": "Discuter", + "listsBtn": "Listes", + "bulletinsBtn": "Bulletins", + "puzzleGameBtn": "Puzzle", + "addressLabel": "Adresse", + "displayNameLabel": "Pseudo", + "blockBtn": "Block Peer", + "savePeerHistory": "Save Peer History", + "savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.", + "dontSavePeerHistory": "Delete Peer History", + "unblockBtn": "Unblock Peer", + "addProfileTitle": "Add new profile", + "editProfileTitle": "Edit Profile", + "profileName": "Display name", + "defaultProfileName": "Alice", + "newProfile": "New Profile", + "editProfile": "Edit Profille", + "radioUsePassword": "Password", + "radioNoPassword": "Unencrypted (No password)", + "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", + "yourDisplayName": "Your Display Name", + "currentPasswordLabel": "Current Password", + "password1Label": "Password", + "password2Label": "Reenter password", + "passwordErrorEmpty": "Password cannot be empty", + "createProfileBtn": "Create Profile", + "saveProfileBtn": "Save Profile", + "passwordErrorMatch": "Passwords do not match", + "passwordChangeError": "Error changing password: Supplied password rejected", + "deleteProfileBtn": "Delete Profile", + "deleteConfirmLabel": "Type DELETE to confirm", + "deleteProfileConfirmBtn": "Really Delete Profile", + "deleteConfirmText": "DELETE", + "addNewProfileBtn": "Add new profile", + "enterProfilePassword": "Enter a password to view your profiles", + "password": "Password", + "error0ProfilesLoadedForPassword": "0 profiles loaded with that password", + "yourProfiles": "Your Profiles", + "yourServers": "Your Servers", + "unlock": "Unlock", + "cwtchSettingsTitle": "Préférences Cwtch", + "versionBuilddate": "Version: %1 Built on: %2", + "zoomLabel": "Interface zoom (essentiellement la taille du texte et des composants de l'interface)", + "blockUnknownLabel": "Block Unknown Peers", + "settingLanguage": "Language", + "localeEn": "English", + "localeFr": "Frances", + "localePt": "Portuguesa", + "localeDe": "Deutsche", + "settingInterfaceZoom": "Zoom level", + "largeTextLabel": "Large", + "settingTheme": "Theme", + "themeLight": "Light", + "themeDark": "Dark", + "experimentsEnabled": "Enable Experiments", + "versionTor": "Version %1 with tor %2", + "version": "Version %1", + "builddate": "Built on: %2", + "defaultScalingText": "Taille par défaut du texte (échelle:", + "smallTextLabel": "Petit", + "loadingTor": "Loading tor...", + "viewGroupMembershipTooltip": "View Group Membership", + "networkStatusDisconnected": "Disconnected from the internet, check your connection", + "networkStatusAttemptingTor": "Attempting to connect to Tor network", + "networkStatusConnecting": "Connecting to network and peers...", + "networkStatusOnline": "Online", + "newConnectionPaneTitle": "New Connection", + "addListItem": "Ajouter un nouvel élément", + "addNewItem": "Ajouter un nouvel élément à la liste", + "todoPlaceholder": "A faire...", + "localeEs": "Espanol", + "localeIt": "Italiana", + "enableGroups": "Enable Group Chat", + "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", + "conversationSettings": "Conversation Settings", + "invalidImportString": "Invalid import string", + "contactAlreadyExists": "Contact Already Exists", + "tooltipOpenSettings": "Open the settings pane", + "tooltipAddContact": "Add a new contact or conversation", + "titleManageContacts": "Conversations", + "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageProfiles": "Manage Cwtch Profiles", + "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", + "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", + "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", + "successfullAddedContact": "Successfully added ", + "dateRightNow": "Right Now", + "dateMinutesAgo": "Minutes Ago", + "dateHoursAgo": "Hours Ago", + "dateDaysAgo": "Days Ago", + "dateWeeksAgo": "Weeks Ago", + "dateLastMonth": "Last Month", + "dateYesterday": "Yesterday", + "dateLastYear": "Last Year", + "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", + "dateNever": "Never", + "dateMonthsAgo": "Months Ago", + "titleManageServers": "Manage Servers", + "inviteToGroup": "You have been invited to join a group:", + "leaveGroup": "Leave This Conversation", + "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", + "yesLeave": "Yes, Leave This Conversation", + "newPassword": "New Password", + "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", + "accepted": "Accepted!", + "rejected": "Rejected!", + "contactSuggestion": "This is a contact suggestion for: ", + "sendAnInvitation": "You sent an invitation for: ", + "torVersion": "Tor Version", + "torStatus": "Tor Status", + "resetTor": "Reset", + "cancel": "Cancel", + "sendMessage": "Send Message", + "sendInvite": "Send a contact or group invite", + "deleteProfileSuccess": "Successfully deleted profile", + "addServerFirst": "You need to add a server before you can create a group", + "nickChangeSuccess": "Profile nickname changed successfully", + "createProfileToBegin": "Please create or unlock a profile to begin", + "addContactFirst": "Add or pick a contact to begin chatting.", + "torNetworkStatus": "Tor network status", + "debugLog": "Turn on console debug logging", + "profileDeleteSuccess": "Successfully deleted profile", + "malformedMessage": "Malformed message" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 44a26cc..e1310fd 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,170 +1,186 @@ { - "@@locale": "it", - "accepted": "", - "acceptGroupBtn": "Accetta", - "acceptGroupInviteLabel": "Vuoi accettare l'invito a", - "acknowledgedLabel": "Riconosciuto", - "addListItem": "Aggiungi un nuovo elemento alla lista", - "addListItemBtn": "Aggiungi elemento", - "addNewItem": "Aggiungi un nuovo elemento alla lista", - "addNewProfileBtn": "Aggiungi nuovo profilo", - "addPeer": "Aggiungi peer", - "addPeerTab": "Aggiungi un peer", - "addProfileTitle": "Aggiungi nuovo profilo", - "addressLabel": "Indirizzo", - "blockBtn": "Blocca il peer", - "blocked": "Bloccato", - "blockUnknownLabel": "Blocca peer sconosciuti", - "builddate": "Costruito il: %2", - "bulletinsBtn": "Bollettini", - "chatBtn": "Chat", - "chatHistoryDefault": "", - "contactAlreadyExists": "", - "contactSuggestion": "", - "conversationSettings": "", - "copiedClipboardNotification": "Copiato negli Appunti", - "copiedToClipboardNotification": "Copiato negli Appunti", - "copyBtn": "Copia", - "couldNotSendMsgError": "Impossibile inviare questo messaggio", - "createGroup": "Crea un gruppo", - "createGroupBtn": "Crea", - "createGroupTab": "Crea un gruppo", - "createGroupTitle": "Crea un gruppo", - "createProfileBtn": "Crea un profilo", - "currentPasswordLabel": "Password corrente", - "cwtchSettingsTitle": "Impostazioni di Cwtch", - "cycleCatsAndroid": "Fare clic per scorrere le categorie.\\nPressione lunga per resettare.", - "cycleCatsDesktop": "Fare clic per scorrere le categorie.\\nCliccare con il tasto destro per resettare.", - "cycleColoursAndroid": "Fare clic per scorrere i colori.\\nPressione lunga per resettare.", - "cycleColoursDesktop": "Fare clic per scorrere i colori.\\nCliccare con il tasto destro per resettare.", - "cycleMorphsAndroid": "Fare clic per scorrere i morph.\\nPressione lunga per resettare.", - "cycleMorphsDesktop": "Fare clic per scorrere i morph.\\nCliccare con il tasto destro per resettare.", - "dateDaysAgo": "", - "dateHoursAgo": "", - "dateLastMonth": "", - "dateLastYear": "", - "dateMinutesAgo": "", - "dateMonthsAgo": "", - "dateNever": "", - "dateRightNow": "", - "dateWeeksAgo": "", - "dateYearsAgo": "", - "dateYesterday": "", - "defaultGroupName": "Gruppo fantastico", - "defaultProfileName": "Alice", - "defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:", - "deleteBtn": "Elimina", - "deleteConfirmLabel": "Digita ELIMINA per confermare", - "deleteConfirmText": "ELIMINA", - "deleteProfileBtn": "Elimina profilo", - "deleteProfileConfirmBtn": "Elimina realmente il profilo", - "descriptionBlockUnknownConnections": "", - "descriptionExperiments": "", - "descriptionExperimentsGroups": "", - "displayNameLabel": "Nome visualizzato", - "dmTooltip": "Clicca per inviare un Messagio Diretto", - "dontSavePeerHistory": "Elimina cronologia dei peer", - "editProfile": "Modifica profilo", - "editProfileTitle": "Modifica profilo", - "enableGroups": "", - "enterCurrentPasswordForDelete": "", - "enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili", - "error0ProfilesLoadedForPassword": "0 profili caricati con quella password", - "experimentsEnabled": "Esperimenti abilitati", - "groupAddr": "Indirizzo", - "groupName": "Nome del gruppo", - "groupNameLabel": "Nome del gruppo", - "invalidImportString": "", - "invitation": "Invito", - "invitationLabel": "Invito", - "inviteBtn": "Invitare", - "inviteToGroup": "", - "inviteToGroupLabel": "Invitare nel gruppo", - "joinGroup": "Unisciti al gruppo", - "joinGroupTab": "Unisciti a un gruppo", - "largeTextLabel": "Grande", - "leaveGroup": "", - "listsBtn": "Liste", - "loadingTor": "Caricamento di tor...", - "localeDe": "Tedesco", - "localeEn": "Inglese", - "localeEs": "Spagnolo", - "localeFr": "Francese", - "localeIt": "Italiano", - "localePt": "Portoghese", - "membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.", - "networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor", - "networkStatusConnecting": "Connessione alla rete e ai peer ...", - "networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione", - "networkStatusOnline": "Online", - "newBulletinLabel": "Nuovo bollettino", - "newConnectionPaneTitle": "Nuova connessione", - "newGroupBtn": "Crea un nuovo gruppo", - "newPassword": "", - "newProfile": "Nuovo profilo", - "noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati", - "password": "Password", - "password1Label": "Password", - "password2Label": "Reinserire la password", - "passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata", - "passwordErrorEmpty": "La password non può essere vuota", - "passwordErrorMatch": "Le password non corrispondono", - "pasteAddressToAddContact": "... incolla qui un indirizzo per aggiungere un contatto...", - "peerAddress": "Indirizzo", - "peerBlockedMessage": "Il peer è bloccato", - "peerName": "Nome", - "peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.", - "peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento", - "pendingLabel": "In corso", - "postNewBulletinLabel": "Pubblica un nuovo bollettino", - "profileName": "Nome visualizzato", - "profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi", - "puzzleGameBtn": "Gioco di puzzle", - "radioNoPassword": "Non criptato (senza password)", - "radioUsePassword": "Password", - "reallyLeaveThisGroupPrompt": "", - "rejected": "", - "rejectGroupBtn": "Rifiuta", - "saveBtn": "Salva", - "savePeerHistory": "Salva cronologia peer", - "savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.", - "saveProfileBtn": "Salva il profilo", - "search": "Ricerca...", - "searchList": "Cerca nella lista", - "sendAnInvitation": "", - "server": "Server", - "serverConnectivityConnected": "Server connesso", - "serverConnectivityDisconnected": "Server disconnesso", - "serverInfo": "Informazioni sul server", - "serverLabel": "Server", - "serverNotSynced": "Non sincronizzato", - "serverSynced": "Sincronizzato", - "settingInterfaceZoom": "Livello di zoom", - "settingLanguage": "Lingua", - "settingTheme": "Tema", - "smallTextLabel": "Piccolo", - "successfullAddedContact": "", - "themeDark": "Scuro", - "themeLight": "Chiaro", - "titleManageContacts": "", - "titleManageProfiles": "", - "titleManageServers": "", - "titlePlaceholder": "titolo...", - "todoPlaceholder": "Da fare...", - "tooltipAddContact": "", - "tooltipOpenSettings": "", - "tooltipUnlockProfiles": "", - "unblockBtn": "Sblocca il peer", - "unlock": "Sblocca", - "update": "Aggiornamento", - "version": "Versione %1", - "versionBuilddate": "Versione: %1 Costruito il: %2", - "versionTor": "Versione %1 con tor %2", - "viewGroupMembershipTooltip": "Visualizza i membri del gruppo", - "viewServerInfo": "Informazioni sul server", - "yesLeave": "", - "yourDisplayName": "Il tuo nome visualizzato", - "yourProfiles": "I tuoi profili", - "yourServers": "I tuoi server", - "zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)" + "@@locale": "it", + "@@last_modified": "2021-06-15T02:08:49+02:00", + "createGroupTitle": "Crea un gruppo", + "serverLabel": "Server", + "groupNameLabel": "Nome del gruppo", + "defaultGroupName": "Gruppo fantastico", + "createGroupBtn": "Crea", + "profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi", + "copyBtn": "Copia", + "copiedToClipboardNotification": "Copiato negli appunti", + "addPeerTab": "Aggiungi un peer", + "createGroupTab": "Crea un gruppo", + "joinGroupTab": "Unisciti a un gruppo", + "peerAddress": "Indirizzo", + "peerName": "Nome", + "groupName": "Nome del gruppo", + "server": "Server", + "invitation": "Invito", + "groupAddr": "Indirizzo", + "addPeer": "Aggiungi peer", + "createGroup": "Crea un gruppo", + "joinGroup": "Unisciti al gruppo", + "newBulletinLabel": "Nuovo bollettino", + "postNewBulletinLabel": "Pubblica un nuovo bollettino", + "titlePlaceholder": "titolo...", + "pasteAddressToAddContact": "... incolla qui un indirizzo per aggiungere un contatto...", + "blocked": "Bloccato", + "cycleCatsAndroid": "Fare clic per scorrere le categorie.\nPressione lunga per resettare.", + "cycleCatsDesktop": "Fare clic per scorrere le categorie.\nCliccare con il tasto destro per resettare.", + "cycleMorphsAndroid": "Fare clic per scorrere i morph.\nPressione lunga per resettare.", + "cycleMorphsDesktop": "Fare clic per scorrere i morph.\nCliccare con il tasto destro per resettare.", + "cycleColoursAndroid": "Fare clic per scorrere i colori.\nPressione lunga per resettare.", + "cycleColoursDesktop": "Fare clic per scorrere i colori.\nCliccare con il tasto destro per resettare.", + "search": "Ricerca...", + "invitationLabel": "Invito", + "serverInfo": "Informazioni sul server", + "serverConnectivityConnected": "Server connesso", + "serverConnectivityDisconnected": "Server disconnesso", + "serverSynced": "Sincronizzato", + "serverNotSynced": "Non sincronizzato", + "viewServerInfo": "Informazioni sul server", + "saveBtn": "Salva", + "inviteToGroupLabel": "Invitare nel gruppo", + "inviteBtn": "Invitare", + "deleteBtn": "Elimina", + "update": "Aggiornamento", + "searchList": "Cerca nella lista", + "peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.", + "addListItemBtn": "Aggiungi elemento", + "membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.", + "dmTooltip": "Clicca per inviare un Messagio Diretto", + "couldNotSendMsgError": "Impossibile inviare questo messaggio", + "acknowledgedLabel": "Riconosciuto", + "pendingLabel": "In corso", + "peerBlockedMessage": "Il peer è bloccato", + "peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento", + "copiedClipboardNotification": "Copiato negli Appunti", + "newGroupBtn": "Crea un nuovo gruppo", + "acceptGroupInviteLabel": "Vuoi accettare l'invito a", + "acceptGroupBtn": "Accetta", + "rejectGroupBtn": "Rifiuta", + "chatBtn": "Chat", + "listsBtn": "Liste", + "bulletinsBtn": "Bollettini", + "puzzleGameBtn": "Gioco di puzzle", + "addressLabel": "Indirizzo", + "displayNameLabel": "Nome visualizzato", + "blockBtn": "Blocca il peer", + "savePeerHistory": "Salva cronologia peer", + "savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.", + "dontSavePeerHistory": "Elimina cronologia dei peer", + "unblockBtn": "Sblocca il peer", + "addProfileTitle": "Aggiungi nuovo profilo", + "editProfileTitle": "Modifica profilo", + "profileName": "Nome visualizzato", + "defaultProfileName": "Alice", + "newProfile": "Nuovo profilo", + "editProfile": "Modifica profilo", + "radioUsePassword": "Password", + "radioNoPassword": "Non criptato (senza password)", + "noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati", + "yourDisplayName": "Il tuo nome visualizzato", + "currentPasswordLabel": "Password corrente", + "password1Label": "Password", + "password2Label": "Reinserire la password", + "passwordErrorEmpty": "La password non può essere vuota", + "createProfileBtn": "Crea un profilo", + "saveProfileBtn": "Salva il profilo", + "passwordErrorMatch": "Le password non corrispondono", + "passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata", + "deleteProfileBtn": "Elimina profilo", + "deleteConfirmLabel": "Digita ELIMINA per confermare", + "deleteProfileConfirmBtn": "Elimina realmente il profilo", + "deleteConfirmText": "ELIMINA", + "addNewProfileBtn": "Aggiungi nuovo profilo", + "enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili", + "password": "Password", + "error0ProfilesLoadedForPassword": "0 profili caricati con quella password", + "yourProfiles": "I tuoi profili", + "yourServers": "I tuoi server", + "unlock": "Sblocca", + "cwtchSettingsTitle": "Impostazioni di Cwtch", + "versionBuilddate": "Versione: %1 Costruito il: %2", + "zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)", + "blockUnknownLabel": "Blocca peer sconosciuti", + "settingLanguage": "Lingua", + "localeEn": "Inglese", + "localeFr": "Francese", + "localePt": "Portoghese", + "localeDe": "Tedesco", + "settingInterfaceZoom": "Livello di zoom", + "largeTextLabel": "Grande", + "settingTheme": "Tema", + "themeLight": "Chiaro", + "themeDark": "Scuro", + "experimentsEnabled": "Esperimenti abilitati", + "versionTor": "Versione %1 con tor %2", + "version": "Versione %1", + "builddate": "Costruito il: %2", + "defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:", + "smallTextLabel": "Piccolo", + "loadingTor": "Caricamento di tor...", + "viewGroupMembershipTooltip": "Visualizza i membri del gruppo", + "networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione", + "networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor", + "networkStatusConnecting": "Connessione alla rete e ai peer ...", + "networkStatusOnline": "Online", + "newConnectionPaneTitle": "Nuova connessione", + "addListItem": "Aggiungi un nuovo elemento alla lista", + "addNewItem": "Aggiungi un nuovo elemento alla lista", + "todoPlaceholder": "Da fare...", + "localeEs": "Spagnolo", + "localeIt": "Italiano", + "enableGroups": "Enable Group Chat", + "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", + "conversationSettings": "Conversation Settings", + "invalidImportString": "Invalid import string", + "contactAlreadyExists": "Contact Already Exists", + "tooltipOpenSettings": "Open the settings pane", + "tooltipAddContact": "Add a new contact or conversation", + "titleManageContacts": "Conversations", + "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageProfiles": "Manage Cwtch Profiles", + "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", + "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", + "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", + "successfullAddedContact": "Successfully added ", + "dateRightNow": "Right Now", + "dateMinutesAgo": "Minutes Ago", + "dateHoursAgo": "Hours Ago", + "dateDaysAgo": "Days Ago", + "dateWeeksAgo": "Weeks Ago", + "dateLastMonth": "Last Month", + "dateYesterday": "Yesterday", + "dateLastYear": "Last Year", + "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", + "dateNever": "Never", + "dateMonthsAgo": "Months Ago", + "titleManageServers": "Manage Servers", + "inviteToGroup": "You have been invited to join a group:", + "leaveGroup": "Leave This Conversation", + "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", + "yesLeave": "Yes, Leave This Conversation", + "newPassword": "New Password", + "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", + "accepted": "Accepted!", + "rejected": "Rejected!", + "contactSuggestion": "This is a contact suggestion for: ", + "sendAnInvitation": "You sent an invitation for: ", + "torVersion": "Tor Version", + "torStatus": "Tor Status", + "resetTor": "Reset", + "cancel": "Cancel", + "sendMessage": "Send Message", + "sendInvite": "Send a contact or group invite", + "deleteProfileSuccess": "Successfully deleted profile", + "addServerFirst": "You need to add a server before you can create a group", + "nickChangeSuccess": "Profile nickname changed successfully", + "createProfileToBegin": "Please create or unlock a profile to begin", + "addContactFirst": "Add or pick a contact to begin chatting.", + "torNetworkStatus": "Tor network status", + "debugLog": "Turn on console debug logging", + "profileDeleteSuccess": "Successfully deleted profile", + "malformedMessage": "Malformed message" } \ No newline at end of file diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index acfbd49..aff75c9 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,170 +1,186 @@ { - "@@locale": "pt", - "accepted": "", - "acceptGroupBtn": "Aceitar", - "acceptGroupInviteLabel": "Você quer aceitar o convite para", - "acknowledgedLabel": "Confirmada", - "addListItem": "Adicionar Item à Lista", - "addListItemBtn": "", - "addNewItem": "Adicionar novo item à lista", - "addNewProfileBtn": "", - "addPeer": "", - "addPeerTab": "", - "addProfileTitle": "", - "addressLabel": "Endereço", - "blockBtn": "", - "blocked": "", - "blockUnknownLabel": "", - "builddate": "", - "bulletinsBtn": "Boletins", - "chatBtn": "Chat", - "chatHistoryDefault": "", - "contactAlreadyExists": "", - "contactSuggestion": "", - "conversationSettings": "", - "copiedClipboardNotification": "Copiado", - "copiedToClipboardNotification": "Copiado", - "copyBtn": "Copiar", - "couldNotSendMsgError": "Não deu para enviar esta mensagem", - "createGroup": "", - "createGroupBtn": "Criar", - "createGroupTab": "", - "createGroupTitle": "Criar Grupo", - "createProfileBtn": "", - "currentPasswordLabel": "", - "cwtchSettingsTitle": "Configurações do Cwtch", - "cycleCatsAndroid": "", - "cycleCatsDesktop": "", - "cycleColoursAndroid": "", - "cycleColoursDesktop": "", - "cycleMorphsAndroid": "", - "cycleMorphsDesktop": "", - "dateDaysAgo": "", - "dateHoursAgo": "", - "dateLastMonth": "", - "dateLastYear": "", - "dateMinutesAgo": "", - "dateMonthsAgo": "", - "dateNever": "", - "dateRightNow": "", - "dateWeeksAgo": "", - "dateYearsAgo": "", - "dateYesterday": "", - "defaultGroupName": "Grupo incrível", - "defaultProfileName": "", - "defaultScalingText": "Texto tamanho padrão (fator de escala: ", - "deleteBtn": "Deletar", - "deleteConfirmLabel": "", - "deleteConfirmText": "", - "deleteProfileBtn": "", - "deleteProfileConfirmBtn": "", - "descriptionBlockUnknownConnections": "", - "descriptionExperiments": "", - "descriptionExperimentsGroups": "", - "displayNameLabel": "Nome de Exibição", - "dmTooltip": "Clique para DM", - "dontSavePeerHistory": "", - "editProfile": "", - "editProfileTitle": "", - "enableGroups": "", - "enterCurrentPasswordForDelete": "", - "enterProfilePassword": "", - "error0ProfilesLoadedForPassword": "", - "experimentsEnabled": "", - "groupAddr": "", - "groupName": "", - "groupNameLabel": "Nome do Grupo", - "invalidImportString": "", - "invitation": "", - "invitationLabel": "Convite", - "inviteBtn": "Convidar", - "inviteToGroup": "", - "inviteToGroupLabel": "Convidar ao grupo", - "joinGroup": "", - "joinGroupTab": "", - "largeTextLabel": "Grande", - "leaveGroup": "", - "listsBtn": "Listas", - "loadingTor": "", - "localeDe": "", - "localeEn": "", - "localeEs": "", - "localeFr": "", - "localeIt": "", - "localePt": "", - "membershipDescription": "A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo.", - "networkStatusAttemptingTor": "", - "networkStatusConnecting": "", - "networkStatusDisconnected": "", - "networkStatusOnline": "", - "newBulletinLabel": "Novo Boletim", - "newConnectionPaneTitle": "", - "newGroupBtn": "Criar novo grupo", - "newPassword": "", - "newProfile": "", - "noPasswordWarning": "", - "password": "", - "password1Label": "", - "password2Label": "", - "passwordChangeError": "", - "passwordErrorEmpty": "", - "passwordErrorMatch": "", - "pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…", - "peerAddress": "", - "peerBlockedMessage": "", - "peerName": "", - "peerNotOnline": "", - "peerOfflineMessage": "", - "pendingLabel": "Pendente", - "postNewBulletinLabel": "Postar novo boletim", - "profileName": "", - "profileOnionLabel": "", - "puzzleGameBtn": "Jogo de Adivinhação", - "radioNoPassword": "", - "radioUsePassword": "", - "reallyLeaveThisGroupPrompt": "", - "rejected": "", - "rejectGroupBtn": "Recusar", - "saveBtn": "Salvar", - "savePeerHistory": "", - "savePeerHistoryDescription": "", - "saveProfileBtn": "", - "search": "", - "searchList": "", - "sendAnInvitation": "", - "server": "", - "serverConnectivityConnected": "", - "serverConnectivityDisconnected": "", - "serverInfo": "", - "serverLabel": "Servidor", - "serverNotSynced": "", - "serverSynced": "", - "settingInterfaceZoom": "", - "settingLanguage": "", - "settingTheme": "", - "smallTextLabel": "Pequeno", - "successfullAddedContact": "", - "themeDark": "", - "themeLight": "", - "titleManageContacts": "", - "titleManageProfiles": "", - "titleManageServers": "", - "titlePlaceholder": "título…", - "todoPlaceholder": "Afazer…", - "tooltipAddContact": "", - "tooltipOpenSettings": "", - "tooltipUnlockProfiles": "", - "unblockBtn": "", - "unlock": "", - "update": "", - "version": "", - "versionBuilddate": "", - "versionTor": "", - "viewGroupMembershipTooltip": "", - "viewServerInfo": "", - "yesLeave": "", - "yourDisplayName": "", - "yourProfiles": "", - "yourServers": "", - "zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)" + "@@locale": "pt", + "@@last_modified": "2021-06-15T02:08:49+02:00", + "createGroupTitle": "Criar Grupo", + "serverLabel": "Servidor", + "groupNameLabel": "Nome do Grupo", + "defaultGroupName": "Grupo incrível", + "createGroupBtn": "Criar", + "profileOnionLabel": "Send this address to peers you want to connect with", + "copyBtn": "Copiar", + "copiedToClipboardNotification": "Copiado", + "addPeerTab": "Add a peer", + "createGroupTab": "Create a group", + "joinGroupTab": "Join a group", + "peerAddress": "Address", + "peerName": "Name", + "groupName": "Group name", + "server": "Server", + "invitation": "Invitation", + "groupAddr": "Address", + "addPeer": "Add Peer", + "createGroup": "Create group", + "joinGroup": "Join group", + "newBulletinLabel": "Novo Boletim", + "postNewBulletinLabel": "Postar novo boletim", + "titlePlaceholder": "título…", + "pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…", + "blocked": "Blocked", + "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", + "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", + "cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.", + "cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.", + "cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.", + "cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.", + "search": "Search...", + "invitationLabel": "Convite", + "serverInfo": "Server Information", + "serverConnectivityConnected": "Server Connected", + "serverConnectivityDisconnected": "Server Disconnected", + "serverSynced": "Synced", + "serverNotSynced": "Out of Sync", + "viewServerInfo": "Server Info", + "saveBtn": "Salvar", + "inviteToGroupLabel": "Convidar ao grupo", + "inviteBtn": "Convidar", + "deleteBtn": "Deletar", + "update": "Update", + "searchList": "Search List", + "peerNotOnline": "Peer is Offline. Applications cannot be used right now.", + "addListItemBtn": "Add Item", + "membershipDescription": "A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo.", + "dmTooltip": "Clique para DM", + "couldNotSendMsgError": "Não deu para enviar esta mensagem", + "acknowledgedLabel": "Confirmada", + "pendingLabel": "Pendente", + "peerBlockedMessage": "Peer is blocked", + "peerOfflineMessage": "Peer is offline, messages can't be delivered right now", + "copiedClipboardNotification": "Copiado", + "newGroupBtn": "Criar novo grupo", + "acceptGroupInviteLabel": "Você quer aceitar o convite para", + "acceptGroupBtn": "Aceitar", + "rejectGroupBtn": "Recusar", + "chatBtn": "Chat", + "listsBtn": "Listas", + "bulletinsBtn": "Boletins", + "puzzleGameBtn": "Jogo de Adivinhação", + "addressLabel": "Endereço", + "displayNameLabel": "Nome de Exibição", + "blockBtn": "Block Peer", + "savePeerHistory": "Save Peer History", + "savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.", + "dontSavePeerHistory": "Delete Peer History", + "unblockBtn": "Unblock Peer", + "addProfileTitle": "Add new profile", + "editProfileTitle": "Edit Profile", + "profileName": "Display name", + "defaultProfileName": "Alice", + "newProfile": "New Profile", + "editProfile": "Edit Profille", + "radioUsePassword": "Password", + "radioNoPassword": "Unencrypted (No password)", + "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", + "yourDisplayName": "Your Display Name", + "currentPasswordLabel": "Current Password", + "password1Label": "Password", + "password2Label": "Reenter password", + "passwordErrorEmpty": "Password cannot be empty", + "createProfileBtn": "Create Profile", + "saveProfileBtn": "Save Profile", + "passwordErrorMatch": "Passwords do not match", + "passwordChangeError": "Error changing password: Supplied password rejected", + "deleteProfileBtn": "Delete Profile", + "deleteConfirmLabel": "Type DELETE to confirm", + "deleteProfileConfirmBtn": "Really Delete Profile", + "deleteConfirmText": "DELETE", + "addNewProfileBtn": "Add new profile", + "enterProfilePassword": "Enter a password to view your profiles", + "password": "Password", + "error0ProfilesLoadedForPassword": "0 profiles loaded with that password", + "yourProfiles": "Your Profiles", + "yourServers": "Your Servers", + "unlock": "Unlock", + "cwtchSettingsTitle": "Configurações do Cwtch", + "versionBuilddate": "Version: %1 Built on: %2", + "zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)", + "blockUnknownLabel": "Block Unknown Peers", + "settingLanguage": "Language", + "localeEn": "English", + "localeFr": "Frances", + "localePt": "Portuguesa", + "localeDe": "Deutsche", + "settingInterfaceZoom": "Zoom level", + "largeTextLabel": "Grande", + "settingTheme": "Theme", + "themeLight": "Light", + "themeDark": "Dark", + "experimentsEnabled": "Enable Experiments", + "versionTor": "Version %1 with tor %2", + "version": "Version %1", + "builddate": "Built on: %2", + "defaultScalingText": "Texto tamanho padrão (fator de escala: ", + "smallTextLabel": "Pequeno", + "loadingTor": "Loading tor...", + "viewGroupMembershipTooltip": "View Group Membership", + "networkStatusDisconnected": "Disconnected from the internet, check your connection", + "networkStatusAttemptingTor": "Attempting to connect to Tor network", + "networkStatusConnecting": "Connecting to network and peers...", + "networkStatusOnline": "Online", + "newConnectionPaneTitle": "New Connection", + "addListItem": "Adicionar Item à Lista", + "addNewItem": "Adicionar novo item à lista", + "todoPlaceholder": "Afazer…", + "localeEs": "Espanol", + "localeIt": "Italiana", + "enableGroups": "Enable Group Chat", + "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", + "conversationSettings": "Conversation Settings", + "invalidImportString": "Invalid import string", + "contactAlreadyExists": "Contact Already Exists", + "tooltipOpenSettings": "Open the settings pane", + "tooltipAddContact": "Add a new contact or conversation", + "titleManageContacts": "Conversations", + "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageProfiles": "Manage Cwtch Profiles", + "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", + "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", + "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", + "successfullAddedContact": "Successfully added ", + "dateRightNow": "Right Now", + "dateMinutesAgo": "Minutes Ago", + "dateHoursAgo": "Hours Ago", + "dateDaysAgo": "Days Ago", + "dateWeeksAgo": "Weeks Ago", + "dateLastMonth": "Last Month", + "dateYesterday": "Yesterday", + "dateLastYear": "Last Year", + "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", + "dateNever": "Never", + "dateMonthsAgo": "Months Ago", + "titleManageServers": "Manage Servers", + "inviteToGroup": "You have been invited to join a group:", + "leaveGroup": "Leave This Conversation", + "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", + "yesLeave": "Yes, Leave This Conversation", + "newPassword": "New Password", + "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", + "accepted": "Accepted!", + "rejected": "Rejected!", + "contactSuggestion": "This is a contact suggestion for: ", + "sendAnInvitation": "You sent an invitation for: ", + "torVersion": "Tor Version", + "torStatus": "Tor Status", + "resetTor": "Reset", + "cancel": "Cancel", + "sendMessage": "Send Message", + "sendInvite": "Send a contact or group invite", + "deleteProfileSuccess": "Successfully deleted profile", + "addServerFirst": "You need to add a server before you can create a group", + "nickChangeSuccess": "Profile nickname changed successfully", + "createProfileToBegin": "Please create or unlock a profile to begin", + "addContactFirst": "Add or pick a contact to begin chatting.", + "torNetworkStatus": "Tor network status", + "debugLog": "Turn on console debug logging", + "profileDeleteSuccess": "Successfully deleted profile", + "malformedMessage": "Malformed message" } \ No newline at end of file From 5681a43d6965902bcaf2fe2ae6fc3f91468d78a7 Mon Sep 17 00:00:00 2001 From: erinn Date: Mon, 14 Jun 2021 17:25:24 -0700 Subject: [PATCH 17/17] l10n updates --- README.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index da68d86..58b3164 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,24 @@ After adding a new key and providing/obtaining translations for it, follow the n ### Updating translations -Only Open Privacy staff members can update translations automatically: +Only Open Privacy staff members can update translations. -``` -flutter pub run flutter_lokalise download -v --api-token "" --project-id "" -``` +In Lokalise, hit Download and make sure: -This will download a bundle of translations from Lokalise and convert it to resource files in `lib/l10n/intl_*.arb`. -The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`). +* Format is set to "Flutter (.arb) +* Output filename is set to `l10n/intl_%LANG_ISO%.%FORMAT%` +* Empty translations is set to "Replace with base language" + +Build, download and unzip the output, overwriting `lib/l10n`. The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`). + +### Adding a language + +If a new language has been added to the Lokalise project, two additional manual steps need to be done: + +* Create a new key called `localeXX` for the name of the language +* Add it to the settings pane by updating `getLanguageFull()` in `lib/views/globalsettingsview.dart` + +Then rebuild as normal. ### Using a string @@ -57,6 +67,5 @@ Text(AppLocalizations.of(context)!.stringIdentifer), ### Configuration -API tokens are only available to Open Privacy staff at this time, who will perform the translation updates for you as part of merging your PRs. - With `generate: true` in `pubspec.yaml`, the Flutter build process checks `l10n.yaml` for input/output filenames. +