Compare commits

...

85 Commits

Author SHA1 Message Date
RuLang c8b4f3ec31 update_2022.07.04_small-fix 2022-07-04 11:39:15 +00:00
RuLang 3405859c8e intl_ru_15.06.2022.arb 2022-06-15 09:27:14 +00:00
Sarah Jamie Lewis 644ae502e5 Merge pull request 'formatting_toolbar' (#475) from formatting_toolbar into trunk
Reviewed-on: cwtch.im/cwtch-ui#475
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-15 03:14:17 +00:00
Sarah Jamie Lewis 7bae6485f7 Fixup Formatting PR (Dans Comments) 2022-06-14 18:44:24 -07:00
Sarah Jamie Lewis 04c335e7a4 formatting toolbar 2022-06-14 18:30:04 -07:00
Sarah Jamie Lewis 3961692817 Nicer Quoted Messages 2022-06-13 10:06:06 -07:00
Sarah Jamie Lewis d703a9636f Fix Contact Message Date not displaying date for day old messages 2022-06-13 09:31:25 -07:00
Dan Ballard e4419366a4 Merge pull request 'Click to scroll on Quoted Message / Shorten Text' (#469) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#469
2022-06-10 23:43:09 +00:00
Sarah Jamie Lewis f848316db9 Fix bug preventing scrolling to unread messages 2022-06-10 15:42:54 -07:00
Sarah Jamie Lewis a5b253f185 Merge pull request 'reply_links' (#470) from reply_links into marcia_fixes
Reviewed-on: cwtch.im/cwtch-ui#470
2022-06-10 21:32:13 +00:00
Sarah Jamie Lewis e7c19c7477 Merge pull request 'show down button in messageview when ever scrolling up' (#471) from show_down into marcia_fixes
Reviewed-on: cwtch.im/cwtch-ui#471
2022-06-10 21:31:10 +00:00
Dan Ballard 59df024867 show down button in messageview when ever scrolling up 2022-06-10 14:28:16 -07:00
Sarah Jamie Lewis 65ff084952 make links in replies clickable 2022-06-10 14:21:40 -07:00
Sarah Jamie Lewis b3e11cfffd remove scroll controller from message view 2022-06-10 12:24:38 -07:00
Sarah Jamie Lewis 0c9be47e17 Click to scroll on Quoted Message / Shorten Text 2022-06-10 12:12:43 -07:00
Dan Ballard 3bb3a8736c Merge pull request 'Fix message view title padding in doublecol view' (#468) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#468
2022-06-10 18:16:31 +00:00
Sarah Jamie Lewis 67850e8e4b Fix message view title padding in doublecol view 2022-06-10 10:40:39 -07:00
Dan Ballard c8e896fa51 Merge pull request 'Modal Menu UI Fixes' (#467) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#467
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-09 23:19:13 +00:00
Sarah Jamie Lewis d1e8f71290 fixes for profile buttons 2022-06-09 14:54:48 -07:00
Sarah Jamie Lewis be8646e805 fix padding 2022-06-09 14:30:38 -07:00
Sarah Jamie Lewis 6d42f2c76c make text bold and add additional padding to contacts modal 2022-06-09 14:28:24 -07:00
Sarah Jamie Lewis 8429907650 modal menus design fixes 2022-06-09 14:26:02 -07:00
Sarah Jamie Lewis c3848553d7 Bugfix when resizing app when menu is open 2022-06-09 13:49:38 -07:00
Sarah Jamie Lewis 3c85b8f59e Merge pull request 'Column-wise contact row (marcia feedback)' (#466) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#466
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-09 20:43:10 +00:00
Sarah Jamie Lewis d0e7e6703b Different buttons 2022-06-09 13:40:42 -07:00
Sarah Jamie Lewis 2bc47173f9 more clear contact request 2022-06-09 13:36:59 -07:00
Sarah Jamie Lewis 15c68d8812 remove padding 2022-06-09 13:20:01 -07:00
Sarah Jamie Lewis e76f2883c6 Column-wise contact row (marcia feedback) 2022-06-09 13:10:27 -07:00
Dan Ballard 439b9b874f Merge pull request 'marcia settings fixes' (#462) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#462
2022-05-31 23:31:27 +00:00
Sarah Jamie Lewis f5393cdb79 Merge branch 'trunk' into marcia_fixes 2022-05-31 23:19:49 +00:00
Sarah Jamie Lewis c0f1b674aa marcia settings fixes 2022-05-31 13:37:32 -07:00
Dan Ballard 630713a5e4 Merge pull request 'New Polish Translations' (#460) from pl_intl into trunk
Reviewed-on: cwtch.im/cwtch-ui#460
2022-05-24 18:53:52 +00:00
Sarah Jamie Lewis d10a6df872 Merge branch 'trunk' into pl_intl 2022-05-24 18:16:16 +00:00
Sarah Jamie Lewis 2723a35d44 New Polish Translations 2022-05-24 11:14:42 -07:00
Dan Ballard 427081c937 Merge pull request 'Fix #457 + Formatting' (#459) from fix457 into trunk
Reviewed-on: cwtch.im/cwtch-ui#459
2022-05-11 19:56:11 +00:00
Sarah Jamie Lewis 9d4abc3725 Fix #457 + Formatting 2022-05-11 12:44:24 -07:00
Sarah Jamie Lewis fa52b741bf Merge pull request 'v1.7.1 29' (#454) from pubspecBump into trunk
Reviewed-on: cwtch.im/cwtch-ui#454
2022-05-02 22:12:25 +00:00
Dan Ballard fb86fb6eae v1.7.1 29 2022-05-02 15:07:55 -07:00
Sarah Jamie Lewis 8dd696b6ab Merge pull request 'dont start 'new messages' when convo selected' (#453) from cache3.0 into trunk
Reviewed-on: cwtch.im/cwtch-ui#453
2022-05-01 17:32:16 +00:00
Dan Ballard 001ad854c7 dont start 'new messages' when convo selected 2022-04-30 14:43:45 -07:00
Dan Ballard af5fb678fc Merge pull request 'caching fixes for stability and android' (#450) from cache3.0 into trunk
Reviewed-on: cwtch.im/cwtch-ui#450
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-29 23:37:20 +00:00
Dan Ballard ffa51e83a1 new message marker moved from id to index and now works on old messages 2022-04-29 16:07:52 -07:00
Dan Ballard 441845ed49 Merge pull request 'Fix maximum width of dropdown boxes in settings' (#452) from fix-settings into trunk
Reviewed-on: cwtch.im/cwtch-ui#452
2022-04-29 17:50:18 +00:00
Sarah Jamie Lewis 0146436cb3 Fix maximum width of dropdown boxes in settings 2022-04-29 09:57:26 -07:00
Dan Ballard 0647a2d98d android pre load unsynced messages 2022-04-28 21:28:12 -07:00
Dan Ballard 0bcfe75a63 rework cache android resume based off message count totals, force pre fetch on load message list, tweak new messages bubble behaviour 2022-04-28 08:57:31 -07:00
Dan Ballard ecdcef2192 Merge pull request 'GetMessage* on android; make reply to use message cache; New Messages bubble doesn't reup' (#448) from replyFix into trunk
Reviewed-on: cwtch.im/cwtch-ui#448
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-27 04:51:20 +00:00
Dan Ballard e6c9f7becb GetMessage* on android; make reply to use message cache; New Messages bubble doesn't reup 2022-04-26 21:34:16 -07:00
Sarah Jamie Lewis 9d8f73ac00 Merge pull request 'Format, Context Binding and Check if File Exists in File Bubble' (#447) from file-fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#447
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-26 19:38:28 +00:00
Sarah Jamie Lewis dc78117e1a Format, Context Binding and Check if File Exists in File Bubble 2022-04-26 12:16:48 -07:00
Dan Ballard 59e3220bce Merge pull request 'Debug Info Fix and Dependency Upgrades' (#433) from perf into trunk
Reviewed-on: cwtch.im/cwtch-ui#433
2022-04-21 23:56:32 +00:00
Sarah Jamie Lewis 653ba199bc Merge branch 'trunk' into perf 2022-04-21 23:33:26 +00:00
Sarah Jamie Lewis 1b45205c48 Merge pull request 'nsis uninstall typo reg key' (#438) from winUninstall into trunk
Reviewed-on: cwtch.im/cwtch-ui#438
2022-04-21 23:33:17 +00:00
Dan Ballard 85186b2565 nsis uninstall typo reg key 2022-04-21 16:32:22 -07:00
Sarah Jamie Lewis 3287fa79ff Merge branch 'trunk' into perf 2022-04-21 23:32:07 +00:00
Sarah Jamie Lewis 111d522484 Upgrade lcg to 1.7.1 2022-04-21 16:31:17 -07:00
Sarah Jamie Lewis 20c854bafb Update Translations 2022-04-21 16:14:03 -07:00
Dan Ballard ffdc7b3262 Merge pull request 'winUninstall' (#434) from winUninstall into trunk
Reviewed-on: cwtch.im/cwtch-ui#434
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-21 18:40:31 +00:00
Dan Ballard a3d986d9d6 ffi on windows more options to detect tor; nsis installer warn about cwtch needing exiting 2022-04-20 18:20:44 -07:00
Sarah Jamie Lewis 5e3387ec8a Debug Info Fix an Dependency Upgrades 2022-04-20 17:28:38 -07:00
Dan Ballard a6c7682c84 nsis windows installer detect running, ask to not, abort 2022-04-20 14:25:36 -07:00
Dan Ballard b29836ed3b register uninstaller with windows add/remove programs 2022-04-20 12:26:28 -07:00
Sarah Jamie Lewis e0bf47b6ab Merge pull request 'a bunch of cache logic fixes and futher support for reconnect on android' (#431) from cachefixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#431
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-20 18:16:47 +00:00
Dan Ballard 4bd92d854f comments 2022-04-19 20:46:59 -07:00
Dan Ballard 82d1bf873f lcg bump 2022-04-19 20:46:59 -07:00
Dan Ballard 5959981fe4 a bunch of cache logic fixes and futher support for reconnect on android 2022-04-19 20:46:59 -07:00
Dan Ballard ab315e289a Merge pull request 'MainActivity return result to not leave dart calls hanging' (#432) from kotlinResult into trunk
Reviewed-on: cwtch.im/cwtch-ui#432
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-20 03:36:20 +00:00
Dan Ballard 6392d67332 MainActivity return result to not leave dart calls hanging 2022-04-19 18:34:22 -07:00
Dan Ballard 8f0b73af2a Merge pull request 'fix linux notification icon (rever to old linux notification manager) and light theme fixes' (#429) from linuxNotif into trunk
Reviewed-on: cwtch.im/cwtch-ui#429
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-15 01:02:30 +00:00
Dan Ballard 4e2f83ccd9 light theme fixes + message cache ! fix 2022-04-14 17:50:53 -07:00
Dan Ballard dc5ba7b392 readd linux notification manager so it handles notification icon in different linux style installs 2022-04-14 17:02:24 -07:00
Sarah Jamie Lewis 3595f5d8d1 Merge pull request 'Debug Info Pane for Desktop' (#428) from debuginfo into trunk
Reviewed-on: cwtch.im/cwtch-ui#428
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-14 22:51:35 +00:00
Sarah Jamie Lewis 1df348c0c1 Debug Info Pane for Desktop 2022-04-14 15:34:36 -07:00
Sarah Jamie Lewis 548e7f4925 Merge pull request 'add android flag secure, pubspec vewrsion bump, and stubs for sdk31 hide overlay' (#427) from androidFlags into trunk
Reviewed-on: cwtch.im/cwtch-ui#427
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-13 21:57:52 +00:00
Dan Ballard a20d2dffc4 add android flag secure, pubspec vewrsion bump, and stubs for sdk31 hide overlay 2022-04-13 14:53:44 -07:00
Dan Ballard 2a712565e9 Merge pull request 'andoird settings / request for power optimization exemption' (#426) from power into trunk
Reviewed-on: cwtch.im/cwtch-ui#426
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
2022-04-13 21:23:41 +00:00
Dan Ballard a94fd3547b add popup about disable battery unoptimized; fix mute policy loading 2022-04-13 14:09:33 -07:00
Dan Ballard c377a09748 add setting that reports / triggers android power optimization exemption 2022-04-13 12:57:15 -07:00
Dan Ballard d261fbd4c0 kotlin powermanagement info and exemption request 2022-04-13 12:53:32 -07:00
Dan Ballard 933ca74fbc Merge pull request 'Themeing Updates including Nicer Code Formatting' (#425) from theme-updates into trunk
Reviewed-on: cwtch.im/cwtch-ui#425
2022-04-12 22:08:20 +00:00
Sarah Jamie Lewis 38f317194d Merge branch 'trunk' into theme-updates 2022-04-12 21:18:58 +00:00
Sarah Jamie Lewis a4ab2ec060 Themeing Updates including Nicer Code Formatting 2022-04-12 14:15:58 -07:00
Dan Ballard 47795094a0 Merge pull request 'Add Hook into Add Contact Flow to better Gauge Intent' (#424) from add_contact_hook into trunk
Reviewed-on: cwtch.im/cwtch-ui#424
2022-04-12 19:27:18 +00:00
Sarah Jamie Lewis 0d1e7bb5a0 Add Hook into Add Contact Flow to better Gauge Intent
(This the future we can expand this, use this information to better guide people)
2022-04-12 12:15:39 -07:00
Sarah Jamie Lewis 987b80c92b Merge pull request 'Message Formatting Experiment Initial Commit' (#413) from message-formatting into trunk
Reviewed-on: cwtch.im/cwtch-ui#413
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-06 22:15:44 +00:00
58 changed files with 1675 additions and 812 deletions

View File

@ -1 +1 @@
2022-04-04-17-46-v1.6.0-15-g97defdf
2022-04-21-19-14-1.7.1

View File

@ -1 +1 @@
2022-04-04-21-46-v1.6.0-15-g97defdf
2022-04-21-23-14-1.7.1

View File

@ -46,6 +46,14 @@
<!--Needed to run in background (lol)-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Ability to ask user to exempt app from power management (which can kill it more frequently especially on some devices.
Allows app to use ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -->
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- TODO when we support sdk 31
<uses-permission-sdk-23 android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
-->
<!--Needed to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" />

View File

@ -1,49 +1,48 @@
package im.cwtch.flwtch
import SplashView
import android.annotation.TargetApi
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.net.Uri
import android.os.PowerManager
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
import android.util.Log
import android.view.Window
import android.view.WindowManager
import androidx.annotation.NonNull
import androidx.lifecycle.Observer
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import io.flutter.embedding.android.SplashScreen
import cwtch.Cwtch
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.SplashScreen
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.ErrorLogResult
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import org.json.JSONObject
import java.util.concurrent.TimeUnit
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import android.net.Uri
import android.provider.DocumentsContract
import android.content.ContentUris
import android.os.Build
import android.os.Environment
import android.database.Cursor
import android.provider.MediaStore
import cwtch.Cwtch
import java.util.concurrent.TimeUnit
class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView()
// Channel to get app info
private val CHANNEL_APP_INFO = "test.flutter.dev/applicationInfo"
private val CALL_APP_INFO = "getNativeLibDir"
private val ANDROID_SETTINGS_CHANNEL_NAME = "androidSettings"
private val ANDROID_SETTINGS_CHANGE_NAME= "androidSettingsChanged"
private var andoidSettingsChangeChannel: MethodChannel? = null
private val CALL_ASK_BATTERY_EXEMPTION = "requestBatteryExemption"
private val CALL_IS_BATTERY_EXEMPT = "isBatteryExempt"
// Channel to get cwtch api calls on
private val CHANNEL_CWTCH = "cwtch"
@ -67,11 +66,26 @@ class MainActivity: FlutterActivity() {
private val FILEPICKER_REQUEST_CODE = 234
private val PREVIEW_EXPORT_REQUEST_CODE = 235
private val PROFILE_EXPORT_REQUEST_CODE = 236
private val REQUEST_DOZE_WHITELISTING_CODE:Int = 9
private var dlToProfile = ""
private var dlToHandle = ""
private var dlToFileKey = ""
private var exportFromPath = ""
override fun onCreate(savedInstanceState: android.os.Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
// Todo: when we support SDK 31
// hideOverlay()
}
/*
@TargetApi(31)
fun hideOverlay() {
window.setHideOverlayWindows(true);
}
*/
// handles clicks received from outside the app (ie, notifications)
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
@ -98,8 +112,16 @@ class MainActivity: FlutterActivity() {
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
super.onActivityResult(requestCode, result, intent);
// has null intent and data
if (requestCode == REQUEST_DOZE_WHITELISTING_CODE) {
// 0 == "battery optimized" (still)
// -1 == "no battery optimization" (exempt!)
andoidSettingsChangeChannel!!.invokeMethod("powerExemptionChange", result == -1)
return;
}
if (intent == null || intent!!.getData() == null) {
Log.i("MainActivity:onActivityResult", "user canceled activity");
Log.i(TAG, "user canceled activity");
return;
}
@ -149,8 +171,10 @@ class MainActivity: FlutterActivity() {
requestWindowFeature(Window.FEATURE_NO_TITLE)
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) }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ANDROID_SETTINGS_CHANNEL_NAME).setMethodCallHandler { call, result -> handleAndroidSettings(call, result) }
notificationClickChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK)
shutdownClickChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_SHUTDOWN_CLICK)
andoidSettingsChangeChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ANDROID_SETTINGS_CHANGE_NAME)
}
// MethodChannel CHANNEL_APP_INFO handler (Flutter Channel for requests for Android environment info)
@ -162,6 +186,30 @@ class MainActivity: FlutterActivity() {
}
}
// MethodChannel ANDROID_SETTINGS_CHANNEL_NAME handler (Flutter Channel for requests for Android settings)
// Called from lib/view/globalsettingsview.dart
private fun handleAndroidSettings(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
CALL_IS_BATTERY_EXEMPT -> result.success(checkIgnoreBatteryOpt() ?: false);
CALL_ASK_BATTERY_EXEMPTION -> { requestBatteryExemption(); result.success(null); }
else -> result.notImplemented()
}
}
@TargetApi(23)
private fun checkIgnoreBatteryOpt(): Boolean {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(this.packageName) ?: false;
}
@TargetApi(23)
private fun requestBatteryExemption() {
val i = Intent()
i.action = ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
i.data = Uri.parse("package:" + this.packageName)
startActivityForResult(i, REQUEST_DOZE_WHITELISTING_CODE);
}
private fun getNativeLibDir(): String {
val ainfo = this.applicationContext.packageManager.getApplicationInfo(
"im.cwtch.flwtch", // Must be app name
@ -249,24 +297,28 @@ class MainActivity: FlutterActivity() {
val count: Int = call.argument("count") ?: 1
result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), count.toLong()))
return
}
"SendMessage" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val message: String = call.argument("message") ?: ""
result.success(Cwtch.sendMessage(profile, conversation.toLong(), message))
return
}
"SendInvitation" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val target: Int = call.argument("target") ?: 0
result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong()))
return
}
"ShareFile" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val filepath: String = call.argument("filepath") ?: ""
result.success(Cwtch.shareFile(profile, conversation.toLong(), filepath))
return
}
"CreateProfile" -> {
@ -289,19 +341,22 @@ class MainActivity: FlutterActivity() {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val indexI: Int = call.argument("index") ?: 0
result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation.toLong(), indexI.toLong())).build())
result.success(Cwtch.getMessage(profile, conversation.toLong(), indexI.toLong()))
return
}
"GetMessageByID" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val id: Int = call.argument("id") ?: 0
result.success(Data.Builder().putString("result", Cwtch.getMessageByID(profile, conversation.toLong(), id.toLong())).build())
result.success(Cwtch.getMessageByID(profile, conversation.toLong(), id.toLong()))
return
}
"GetMessageByContentHash" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val contentHash: String = call.argument("contentHash") ?: ""
result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, conversation.toLong(), contentHash)).build())
result.success(Cwtch.getMessagesByContentHash(profile, conversation.toLong(), contentHash))
return
}
"SetMessageAttribute" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
@ -469,8 +524,10 @@ class MainActivity: FlutterActivity() {
}
}
)
return
}
}
result.success(null)
}
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver
@ -489,9 +546,7 @@ class MainActivity: FlutterActivity() {
// We need to do this here because after a "pause" flutter is still running
// but we might have lost sync with the background process...
Log.i("MainActivity.kt", "Call ReconnectCwtchForeground")
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, "ReconnectCwtchForeground").putString(FlwtchWorker.KEY_ARGS, "{}").build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(applicationContext).enqueue(workRequest)
Cwtch.reconnectCwtchForeground()
}
override fun onStop() {

View File

@ -0,0 +1,61 @@
import 'package:cwtch/third_party/linkify/linkify.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(bcontext)!.clickableLinksWarning),
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinksCopy, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinksCopy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: link.url));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.copiedToClipboardNotification),
);
Navigator.pop(bcontext);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinkOpen, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinkOpen),
onPressed: () async {
if (await canLaunch(link.url)) {
await launch(link.url);
Navigator.pop(bcontext);
} else {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.clickableLinkError),
);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
}
},
),
),
]),
],
)),
));
});
}

View File

@ -118,4 +118,6 @@ abstract class Cwtch {
void l10nInit(String notificationSimple, String notificationConversationInfo);
void dispose();
Future<dynamic> GetDebugInfo();
}

View File

@ -102,6 +102,9 @@ typedef VoidFromStringIntIntFn = void Function(Pointer<Utf8>, int, int, int);
typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function();
typedef void_to_string = Pointer<Utf8> Function();
typedef StringFromVoid = Pointer<Utf8> Function();
const String UNSUPPORTED_OS = "unsupported-os";
class CwtchFfi implements Cwtch {
@ -157,7 +160,13 @@ class CwtchFfi implements Cwtch {
}
} else if (Platform.isWindows) {
cwtchDir = envVars['CWTCH_DIR'] ?? path.join(envVars['UserProfile']!, ".cwtch");
bundledTor = "Tor\\Tor\\tor.exe";
String currentTor = path.join(Directory.current.absolute.path, "Tor\\Tor\\tor.exe");
if (await File(currentTor).exists()) {
bundledTor = currentTor;
} else {
String exeDir = path.dirname(Platform.resolvedExecutable);
bundledTor = path.join(exeDir, "Tor\\Tor\\tor.exe");
}
} else if (Platform.isMacOS) {
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, "Library/Application Support/Cwtch");
if (await File("Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
@ -206,8 +215,9 @@ class CwtchFfi implements Cwtch {
// ignore: non_constant_identifier_names
final StartCwtch = startCwtchC.asFunction<StartCwtchFn>();
final ut8CwtchDir = cwtchDir.toNativeUtf8();
StartCwtch(ut8CwtchDir, ut8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length);
final utf8CwtchDir = cwtchDir.toNativeUtf8();
StartCwtch(utf8CwtchDir, utf8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length);
malloc.free(utf8CwtchDir);
// Spawn an isolate to listen to events from libcwtch-go and then dispatch them when received on main thread to cwtchNotifier
cwtchIsolate = await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort);
@ -317,6 +327,7 @@ class CwtchFfi implements Cwtch {
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
@ -817,4 +828,14 @@ class CwtchFfi implements Cwtch {
malloc.free(utf8file);
return importResult;
}
@override
Future<String> GetDebugInfo() async {
var getDebugInfo = library.lookup<NativeFunction<void_to_string>>("c_GetDebugInfo");
final GetDebugInfo = getDebugInfo.asFunction<StringFromVoid>();
Pointer<Utf8> result = GetDebugInfo();
String debugResult = result.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
return debugResult;
}
}

View File

@ -326,4 +326,11 @@ class CwtchGomobile implements Cwtch {
Future<dynamic> ImportProfile(String file, String pass) {
return cwtchPlatform.invokeMethod("ImportProfile", {"file": file, "pass": pass});
}
@override
Future GetDebugInfo() {
// FIXME: getDebugInfo is less useful for Android so for now
// we don't implement it
return Future.value("{}");
}
}

View File

@ -1,19 +1,24 @@
{
"@@locale": "cy",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"formattingExperiment": "Fformatio Neges",
"clickableLinkOpen": "Agor URL",
"clickableLinksCopy": "Copïo URL",
"shuttingDownApp": "Wrthi'n cau...",
"failedToImportProfile": "Gwall Wrth Fewnforio Proffil",
"importProfile": "Proffil Mewnforio",
"exportProfile": "Proffil Allforio",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",
"clickableLinksCopy": "Copy URL",
"clickableLinkOpen": "Open URL",
"clickableLinksWarning": "Opening this URL will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open URLs from people you trust. Are you sure you want to continue?",
"shuttingDownApp": "Shutting down...",
"successfullyImportedProfile": "Successfully Imported Profile: %profile",
"failedToImportProfile": "Error Importing Profile",
"importProfileTooltip": "Use an encrypted Cwtch backup to bring in a profile created in another instance of Cwtch.",
"importProfile": "Import Profile",
"exportProfileTooltip": "Backup this profile to an encrypted file. The encrypted file can be imported into another Cwtch app.",
"exportProfile": "Export Profile",
"deleteConfirmLabel": "Teipiwch DILEU i gadarnhau",
"deleteConfirmText": "DILEU",
"localeDa": "Daneg",

View File

@ -1,6 +1,11 @@
{
"@@locale": "da",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,19 +1,77 @@
{
"@@locale": "de",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",
"clickableLinksCopy": "Copy URL",
"clickableLinkOpen": "Open URL",
"clickableLinksWarning": "Opening this URL will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open URLs from people you trust. Are you sure you want to continue?",
"shuttingDownApp": "Shutting down...",
"successfullyImportedProfile": "Successfully Imported Profile: %profile",
"failedToImportProfile": "Error Importing Profile",
"importProfileTooltip": "Use an encrypted Cwtch backup to bring in a profile created in another instance of Cwtch.",
"importProfile": "Import Profile",
"exportProfileTooltip": "Backup this profile to an encrypted file. The encrypted file can be imported into another Cwtch app.",
"exportProfile": "Export Profile",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Die Akku Optimierungen können nicht innerhalb von Cwtch wieder aktiviert werden. Bitte gehe zu Android \/ Einstellungen \/ Apps \/ Cwtch \/ Akku und setze die Akku Nutzung auf 'Optimiert'",
"settingAndroidPowerExemptionDescription": "Optional: Fordere Android auf, Cwtch von der optimierten Energieverwaltung auszunehmen. Dies wird zu einer besseren Stabilität auf Kosten eines höheren Batterieverbrauchs führen.",
"settingAndroidPowerExemption": "Android Ignoriere Akku Optimierungen",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Dieses Feature benötigt die Aktivierung der experimentellen Gruppen in den Einstellungen",
"messageFormattingDescription": "Aktiviere Richtext Formatierung in den angezeigten Nachrichten z.B. **fett** und *kursiv*",
"formattingExperiment": "Nachrichten Formatierung",
"clickableLinkError": "Auf Fehler gelaufen beim Versuch die URL zu öffnen",
"clickableLinksCopy": "URL kopieren",
"clickableLinkOpen": "URL öffnen",
"clickableLinksWarning": "Das Öffnen dieser URL wird eine Anwendung außerhalb von Cwtch starten und könnte Metadaten enthüllen oder anderweitig die Sicherheit von Cwtch gefährden. Öffne nur URLs von Personen denen Du vertraust. Bist Du sicher, dass Du fortfahren möchtest?",
"shuttingDownApp": "Herunterfahren...",
"successfullyImportedProfile": "Profil erfolgreich importiert: %profile",
"failedToImportProfile": "Fehler beim Import des Profils",
"importProfileTooltip": "Benutze ein verschlüsseltes Cwtch Backup um ein in einer anderen Cwtch Instanz erzeugtes Profil zu aktivieren.",
"importProfile": "Profil importieren",
"exportProfileTooltip": "Backup des Profils in eine verschlüsselte Datei. Die verschlüsselte Datei kann in eine andere Cwtch App importiert werden.",
"exportProfile": "Profil exportieren",
"conversationNotificationPolicySettingLabel": "Unterhaltungs-Benachrichtungs-Einstellung",
"settingsGroupExperiments": "experimentelle Funktionen",
"notificationPolicySettingDescription": "Voreinstellungen der Benachrichtigungsverhaltens",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Überschreiben der Tor Einstellung. Achtung: gefährlich! Mache das nur, wenn Du weisst, was du machst.",
"torSettingsCustomSocksPortDescription": "Verwende einen eigenen Port für Datenverbindungen zum Tor-Proxy",
"torSettingsEnabledAdvancedDescription": "Einen existierenden Tor-Service auf Ihrem System, oder Parameter des Cwtch Tor Services anpassen.",
"msgAddToAccept": "Füge dieses Konto zu Deinen Kontakten hinzu, um diese Datei zu akzeptieren.",
"msgConfirmSend": "Möchtest Du diese Datei wirklich senden",
"storageMigrationModalMessage": "Profile werden auf das neue Storage-Format migriert. Das kann ein paar Minuten dauern...",
"loadingCwtch": "Lade Cwtch...",
"themeNameMidnight": "Mitternacht",
"themeNameMermaid": "Meerjungfrau",
"themeNamePumpkin": "Kürbis",
"themeNameGhost": "Geist",
"themeNameVampire": "Vampir",
"themeNameWitch": "Hexe",
"settingImagePreviewsDescription": "Bilder werden automatisch heruntergeladen und eine Voransicht erstellt. Voransichten können die Sicherheit gefährden. Du solltest diese experimentelle Einstellung bei nicht vertrauenswürdigen Kontakten nicht aktivieren. Profilbilder sind für Cwtch Version 1.6 geplant.",
"displayNameTooltip": "Einen Anzeigenamen eingeben",
"fileCheckingStatus": "Überprüfung des Download Status",
"plainServerDescription": "Wir empfehlen, dass Du deine Cwtch-Server mit einem Passwort schützst. Wenn Du auf diesem Server kein Kennwort festlegst, kann jeder, der Zugang zu diesem Gerät hat, auf Informationen über diesen Server zugreifen, einschließlich sensibler kryptografischer Schlüssel.",
"enterCurrentPasswordForDeleteServer": "Das aktuelle Passwort um den Server zu entfernen",
"settingServersDescription": "Das experimentelle server hosting ermöglicht das Hosting und die Verwaltung von Cwtch Servern",
"settingServers": "Server hosten",
"serversManagerTitleLong": "Deine Server",
"serverAutostartDescription": "Legt fest, ob die Anwendung den Server beim Start automatisch starten soll",
"descriptionFileSharing": "Der experimentelle Datei Austausch erlaubt Dir Dateien an Cwtch Kontakte oder Gruppen zu senden und zu empfangen. Hinweis, das Teilen einer Datei in einer Gruppe führt dazu, dass alle Mitglieder der Gruppe sich direkt mit Dir über Cwtch verbinden um die Datei herunter zu laden.",
"messageFileOffered": "Kontakt möchte Dir eine Datei senden",
"messageFileSent": "Du hast eine Datei gesendet",
"messageEnableFileSharing": "Aktiviere den experimentellen Dateiaustusch um diese Nachricht zu sehen.",
"retrievingManifestMessage": "Dateiinformation wird geladen...",
"descriptionStreamerMode": "Wenn aktiviert, macht diese Option die App vom Aussehen her privater für Streaming oder Präsenation, z.B. werden Profile und Kontaktadressen ausgeblendet",
"streamerModeLabel": "Streamer\/Präsentationsmodus",
"blockUnknownConnectionsEnabledDescription": "Verbindungen von unbekannten Kontakten sind blockiert. Du kannst dies in Einstellungen ändern",
"placeholderEnterMessage": "Schreibe eine Nachricht...",
"plainProfileDescription": "Wir empfehlen, dass Du Deine Cwtch-Profile mit einem Passwort schützst. Wenn Du kein Passwort für dieses Profil festlegst, kann jeder, der Zugang zu diesem Gerät hat, auf Informationen über dieses Profil zugreifen, einschließlich Kontakte, Nachrichten und sensible kryptographische Schlüssel.",
"encryptedProfileDescription": "Das Verschlüsseln eines Profils mit einem Passwort schützt es vor anderen Personen, die ebenfalls dieses Gerät benutzten könnten. Verschlüsselte Profile können nicht entschlüsselt, angezeigt und benutzt werden bis das korrekte Passwort zum Entsperren eingegeben wurde.",
"settingUIColumnOptionSame": "Gleich wie bei den Hochformat Einstellung",
"settingUIColumnPortrait": "UI Spalten im Hochformat",
"groupInviteSettingsWarning": "Du wurdest eingeladen einer Gruppe beizutreten! Bitte aktiviere die experimentelle Gruppenchat Funktion in den Einstellungen, um diese Einladung anzusehen.",
"debugLog": "Konsolendebuglogging aktivieren",
"descriptionBlockUnknownConnections": "Falls aktiviert, wird diese Einstellung alle Verbindungen von Cwtch Usern automatisch schliessen, wenn sie nicht in deinen Kontakten sind.",
"tooltipOpenSettings": "Öffne das Einstellungsmenü",
"localeIt": "Italienisch",
"localeEs": "Spanisch",
"builddate": "Erstelldatum: %2",
"experimentsEnabled": "Experimentelle Funktionen aktiviert",
"localeDe": "Deutsch",
"localePt": "Portugiesisch",
"localeFr": "Französisch",
"localeEn": "Englisch",
"zoomLabel": "Benutzeroberflächen-Zoom (betrifft hauptsächlich Text- und Button-Größen)",
"profileOnionLabel": "Diese Adresse an Kontakte senden, mit denen Sie sich verbinden möchten",
"acknowledgedLabel": "Bestätigt",
"deleteConfirmLabel": "Gib LÖSCHEN ein, um zu bestätigen",
"localeDa": "Dänisch",
"localeCy": "Walisisch",
@ -42,9 +100,7 @@
"notificationContentContactInfo": "Konversationsinformationen",
"notificationContentSettingDescription": "Steuert den Inhalt von Gesprächsbenachrichtigungen",
"settingsGroupAppearance": "Aussehen",
"conversationNotificationPolicySettingLabel": "Konversation Benachrichtungs Einstellung",
"notificationContentSimpleEvent": "Einfaches Ereignis",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"addPeer": "Kontakt hinzufügen",
"peerNotOnline": "Kontakt ist offline. Die Applikation kann momentan nicht verwendet werden.",
"peerBlockedMessage": "Kontakt ist blockiert",
@ -61,38 +117,22 @@
"localeLb": "Luxemburgisch",
"localeNo": "Norwegisch",
"localeEl": "Griechisch",
"settingsGroupExperiments": "Experimente",
"settingGroupBehaviour": "Verhalten",
"notificationPolicySettingDescription": "Voreinstellungen der Mitteilungsverhalten",
"conversationNotificationPolicyNever": "Niemals",
"labelTorNetwork": "Tor Netzwerk",
"descriptionACNCircuitInfo": "Detailinformationen über den Pfad der anonymisierten Kommunikationsnetzwerkes, der für diese Unterhaltung verwendet wurde.",
"labelACNCircuitInfo": "ACN Circuit Information",
"fileSharingSettingsDownloadFolderTooltip": "Wählen Sie einen anderen Ordner für Downloads.",
"torSettingsErrorSettingPort": "Port Nummer muss zwischen 1 und 65535 sein",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Überschreiben der Tor Einstellung. Achtung: gefährlich! Machen Sie das nur, wenn Sie wissen, was Sie tun.",
"torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy",
"torSettingsCustomSocksPort": "Spezieller SOCKS Port",
"torSettingsEnabledAdvancedDescription": "Ein existierndes Tor-Service auf Ihrem System, oder Parameter des Cwtch Tor Services anpassen.",
"torSettingsEnabledAdvanced": "Erweiterte Tor Konfiguration aktivieren",
"msgAddToAccept": "Fügen Sie dieses Konto zu Ihren Kontakten hinzu, um diese Datei zu akzeptieren.",
"btnSendFile": "Datei senden",
"msgConfirmSend": "Wollen Sie diese Datei wirklich senden",
"msgFileTooBig": "Dateigröße darf nicht größer als 10 GB sein",
"storageMigrationModalMessage": "Profile werden auf das neue Storage-Format migriert. Das kann ein paar Minuteen dauern...",
"loadingCwtch": "Laden Cwtch...",
"themeColorLabel": "Farbthema",
"themeNameNeon2": "Neon2",
"themeNameNeon1": "Neon1",
"themeNameMidnight": "Midnight",
"themeNameMermaid": "Mermaid",
"themeNamePumpkin": "Pumpkin",
"themeNameGhost": "Ghost",
"themeNameVampire": "Vampire",
"themeNameWitch": "Witch",
"themeNameCwtch": "Cwtch",
"settingDownloadFolder": "Download Ordner",
"settingImagePreviewsDescription": "Bilder werden automatisch heruntergeladen und eine Voransicht erstellt. Voransichten können die Ihre Sicherheit gefährden, Sie sollten diese experimentelle Einstellung bei nicht vertrauenswürdigen Kontakten nicht aktivieren. Profilbilder sind für Cwtch Version 1.6 geplant.",
"settingImagePreviews": "Bild Voransichten und Profil Bilder",
"experimentClickableLinksDescription": "Experimentelle Hyperlinks erlauben Ihnen auf URLs in Mitteilungen zu klicken.",
"enableExperimentClickableLinks": "Klickbare Hyperlinks aktivieren",
@ -101,7 +141,6 @@
"serverMetricsLabel": "Server Metriken",
"manageKnownServersShort": "Server",
"manageKnownServersLong": "Bekannte Server verwalten",
"displayNameTooltip": "Einen Display Namen eingeben",
"manageKnownServersButton": "Bekannte Server verwalten",
"fieldDescriptionLabel": "Beschreibung",
"groupsOnThisServerLabel": "Gruppen auf dem Server",
@ -112,25 +151,18 @@
"localeRU": "Russisch",
"copyServerKeys": "Schlüssel kopieren",
"verfiyResumeButton": "Verifizierung\/abschließen",
"fileCheckingStatus": "Überprüfung Download Status",
"fileInterrupted": "Unterbrochen",
"fileSavedTo": "Gespeichert unter",
"encryptedServerDescription": "Das Verschlüsseln eines Servers mit einem Passwort schützt vor anderen Benutzern auf diesem Gerät. Verschlüsselte Server können nicht entschlüsselt, dargestellt oder verbunden werden, bis das korrekte Passwort eingegeben wurde.",
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
"deleteServerConfirmBtn": "Wirklich den Server entfernen",
"deleteServerSuccess": "Server erfolgreich entfernt",
"enterCurrentPasswordForDeleteServer": "Das momentane Passwort um den Server zu entfernen",
"copyAddress": "Adresse kopieren",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Server",
"enterServerPassword": "Passwort um Server zu entsperren",
"unlockProfileTip": "Bitte entsperren oder erstellen Sie ein Profil um zu starten!",
"unlockServerTip": "Bitte entsperren oder erstellen Sie einen Server um zu starten!",
"addServerTooltip": "Neuen Server hinzufügen",
"serversManagerTitleShort": "Server",
"serversManagerTitleLong": "Ihre Server",
"saveServerButton": "Server sichern",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Server starten oder stoppen",
"serverEnabled": "Server aktivieren",
@ -140,34 +172,21 @@
"editServerTitle": "Server editieren",
"addServerTitle": "Server hinzufügen",
"titleManageProfilesShort": "Profile",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "Dateien gemeinsam nutzen",
"tooltipSendFile": "Datei senden",
"messageFileOffered": "Kontakt möchte Ihnen eine Datei senden",
"messageFileSent": "Sie haben eine Datei gesendet",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Dateigröße",
"labelFilename": "Dateiname",
"openFolderButton": "Ordner öffnen",
"retrievingManifestMessage": "Dateiinformation werden geladen...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"streamerModeLabel": "Streamer\/Präsentationismodus",
"archiveConversation": "Diese Unterhaltung archivieren",
"blockUnknownConnectionsEnabledDescription": "Verbindungen von unbekannten Kotakten sind blockiert. Sie können das in Einstellungen ändern",
"showMessageButton": "Nachricht anzeigen",
"blockedMessageMessage": "Diese Nachticht ist von einem blockierten Profil.",
"placeholderEnterMessage": "Schreiben Sie eine Nachricht...",
"plainProfileDescription": "We recommend that you protect your Cwtch profiles with a password. If you do not set a password on this profile then anyone who has access to this device may be able to access information about this profile, including contacts, messages and sensitive cryptographic keys.",
"encryptedProfileDescription": "Encrypting a profile with a password protects it from other people who may also use this device. Encrypted profiles cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"addContactConfirm": "Kontakt hinzufügen %1",
"addContact": "Kontakt hinzufügen",
"contactGoto": "Zur Unterhaltung mit %1",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Doppelt (1:4)",
"settingUIColumnDouble12Ratio": "Doppelt (1:2)",
"settingUIColumnSingle": "Einfach",
"settingUIColumnLandscape": "UI Spalten im Querformat",
"settingUIColumnPortrait": "UI Columns im Hochformat",
"localePl": "Polnisch",
"tooltipRemoveThisQuotedMessage": "Zitierte Nachricht entfernen.",
"tooltipReplyToThisMessage": "Auf diese Nachricht antworten",
@ -180,10 +199,8 @@
"newMessageNotificationSimple": "Neue Nachricht",
"localeRo": "Rumänisch",
"downloadFileButton": "Herunterladen",
"experimentsEnabled": "Experimente aktiviert",
"malformedMessage": "Fehlerhafte Nachricht",
"contactSuggestion": "Dieser Kontaktvorschlag ist für: ",
"descriptionBlockUnknownConnections": "Falls aktiviert, wird diese Einstellung alle Verbindungen von Cwtch Usern autmoatisch schliessen, wenn sie nicht in deinen Kontakten sind.",
"descriptionExperimentsGroups": "Mit experimentellen Gruppen kann Cwtch über nicht vertrauenswürdige Serverinfrastruktur die Kommunikation mit mehr als einem Kontakt vereinfachen.",
"descriptionExperiments": "Experimentelle Cwtch Features sind optionale, opt-in Features für die andere Privatsphärenaspekte berücksichtigt werden als bei traditionellen 1:1 metadatenresistenten Chats, wie z. B. Gruppennachrichten, Bots usw.",
"networkStatusDisconnected": "Vom Internet getrennt, überprüfe deine Verbindung",
@ -194,13 +211,11 @@
"notificationNewMessageFromPeer": "Neue Nachricht von einem Kontakt!",
"tooltipHidePassword": "Password verstecken",
"tooltipShowPassword": "Password anzeigen",
"groupInviteSettingsWarning": "Du wurdest eingeladen einer Gruppe beizutreten! Bitte aktiviere das Gruppenchat Experiment in den Einstellungen um diese Einladung anzusehen.",
"shutdownCwtchAction": "Cwtch schliessen",
"shutdownCwtchDialog": "Bist du sicher, dass du Cwtch schliessen möchtest? Alle Verbindungen werden geschlossen und die App wird beendet.",
"shutdownCwtchDialogTitle": "Cwtch schliessen?",
"shutdownCwtchTooltip": "Cwtch schliessen",
"profileDeleteSuccess": "Profil erfolgreich gelöscht",
"debugLog": "Konsolendebuglogging aktivivieren",
"torNetworkStatus": "Tor Netzwerkstatus",
"addContactFirst": "Wähle einen Kontakt oder füge ihn hinzu, um einen Chat zu starten.",
"createProfileToBegin": "Bitte erstelle oder entsperre ein Profil um loszulegen",
@ -228,17 +243,11 @@
"tooltipUnlockProfiles": "Entsperre verschlüsselte Profile durch Eingabe des Passworts.",
"titleManageContacts": "Unterhaltungen",
"tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen",
"tooltipOpenSettings": "Öfffne das Einstellungsmenü",
"contactAlreadyExists": "Kontakt existiert bereits",
"invalidImportString": "Ungültiger Importstring",
"conversationSettings": "Unterhaltungseinstellungen",
"enterCurrentPasswordForDelete": "Bitte gib das aktuelle Passwort ein, um diese Profil zu löschen.",
"enableGroups": "Gruppenchat aktivieren",
"localeIt": "Italiana",
"localeEs": "Espanol",
"localePt": "Portuguesa",
"localeFr": "Frances",
"localeEn": "English",
"passwordErrorEmpty": "Passwort darf nicht leer sein",
"currentPasswordLabel": "aktuelles Passwort",
"yourDisplayName": "Dein Anzeigename",
@ -290,13 +299,11 @@
"unlock": "Entsperren",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"settingLanguage": "Sprache",
"localeDe": "Deutsche",
"settingInterfaceZoom": "Zoomstufe",
"themeLight": "Licht",
"themeDark": "Dunkel",
"versionTor": "Version %1 mit tor %2",
"version": "Version %1",
"builddate": "Aufgebaut auf: %2",
"loadingTor": "Tor wird geladen...",
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
@ -304,7 +311,6 @@
"smallTextLabel": "Klein",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
"largeTextLabel": "Groß",
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)",
"cwtchSettingsTitle": "Cwtch Einstellungen",
"copiedToClipboardNotification": "in die Zwischenablage kopiert",
"addressLabel": "Adresse",
@ -318,7 +324,6 @@
"newGroupBtn": "Neue Gruppe anlegen",
"copyBtn": "Kopieren",
"pendingLabel": "Bestätigung ausstehend",
"acknowledgedLabel": "bestätigt",
"couldNotSendMsgError": "Nachricht konnte nicht gesendet werden",
"inviteBtn": "Einladen",
"inviteToGroupLabel": "In die Gruppe einladen",

View File

@ -1,6 +1,11 @@
{
"@@locale": "el",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,6 +1,11 @@
{
"@@locale": "en",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,6 +1,11 @@
{
"@@locale": "es",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,8 +1,13 @@
{
"@@locale": "fr",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"settingAndroidPowerExemptionDescription": "Android applique par défaut un profil de gestion de l'énergie \"optimisé\" aux applications, ce qui peut entraîner leur arrêt ou leur suppression. Demandez à Android d'exempter Cwtch de ce profil pour une meilleure stabilité mais une plus grande consommation d'énergie.",
"settingsAndroidPowerReenablePopup": "Impossible de réactiver l'optimisation de la batterie à partir de Cwtch. Veuillez aller dans Android \/ Paramètres \/ Apps \/ Cwtch \/ Batterie et régler l'utilisation sur 'Optimisé'.",
"okButton": "OK",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Cette fonctionnalité nécessite que lexpérience Groupes soit activée dans Paramètres",
"settingAndroidPowerExemption": "Android ignore les optimisations de la batterie",
"messageFormattingDescription": "Activer la mise en forme de texte enrichi dans les messages affichés, par exemple **gras** et *italique*",
"formattingExperiment": "Mise en forme des messages",
"clickableLinksWarning": "L'ouverture de cette URL lancera une application en dehors de Cwtch et peut révéler des métadonnées ou compromettre la sécurité de Cwtch. N'ouvrez que les URLs de personnes en qui vous avez confiance. Êtes-vous sûr de vouloir continuer ?",
"clickableLinksCopy": "Copier l'URL",
"clickableLinkOpen": "Ouvrir l'URL",

View File

@ -1,19 +1,38 @@
{
"@@locale": "it",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",
"clickableLinksCopy": "Copy URL",
"clickableLinkOpen": "Open URL",
"clickableLinksWarning": "Opening this URL will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open URLs from people you trust. Are you sure you want to continue?",
"shuttingDownApp": "Shutting down...",
"successfullyImportedProfile": "Successfully Imported Profile: %profile",
"failedToImportProfile": "Error Importing Profile",
"importProfileTooltip": "Use an encrypted Cwtch backup to bring in a profile created in another instance of Cwtch.",
"importProfile": "Import Profile",
"exportProfileTooltip": "Backup this profile to an encrypted file. The encrypted file can be imported into another Cwtch app.",
"exportProfile": "Export Profile",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"settingsAndroidPowerReenablePopup": "Impossibile riattivare l'ottimizzazione della batteria dall'interno di Cwtch. Vai su Android \/ Impostazioni \/ Apps \/ Cwtch \/ Informazioni App \/ (Utilizzo) Batteria e imposta su 'Ottimizzato'.",
"puzzleGameBtn": "Gioco di puzzle",
"editProfileTitle": "Modifica il profilo",
"currentPasswordLabel": "Password corrente",
"createProfileBtn": "Crea un profilo",
"saveProfileBtn": "Salva il profilo",
"deleteProfileBtn": "Elimina il profilo",
"deleteProfileConfirmBtn": "Elimina definitivamente il profilo",
"yourProfiles": "I tuoi profili",
"yourServers": "I tuoi server",
"titleManageProfiles": "Gestisci i profili Cwtch",
"titleManageServers": "Gestisci i server",
"leaveConversation": "Lascia questa conversazione",
"yesLeave": "Sì, lascia questa conversazione",
"newPassword": "Nuova password",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Questa funzione richiede che l'esperimento Gruppi sia abilitato in Impostazioni",
"importProfileTooltip": "Utilizza un backup Cwtch crittografato per importare un profilo creato in un'altra istanza di Cwtch.",
"clickableLinksWarning": "L'apertura di questo URL avvierà un'applicazione al di fuori di Cwtch e potrebbe rivelare metadati o compromettere in altro modo la sicurezza di Cwtch. Apri solo gli URL provenienti da persone di cui ti fidi. Vuoi continuare?",
"exportProfileTooltip": "Salva il backup di questo profilo in un file crittografato. Il file crittografato può essere importato in un'altra app Cwtch.",
"successfullyImportedProfile": "Profilo importato con successo: %profilo",
"clickableLinkError": "Errore riscontrato durante il tentativo di aprire l'URL",
"messageFormattingDescription": "Abilita la formattazione RTF nei messaggi visualizzati, ad esempio **grassetto** e *corsivo*",
"settingAndroidPowerExemption": "Android ignora le ottimizzazioni della batteria",
"settingAndroidPowerExemptionDescription": "Opzionale: richiedi ad Android di esentare Cwtch dalla gestione ottimizzata dell'alimentazione. Ciò si tradurrà in una migliore stabilità a costo di un maggiore utilizzo della batteria.",
"okButton": "OK",
"exportProfile": "Esporta profilo",
"importProfile": "Importa profilo",
"failedToImportProfile": "Errore nell'importazione del profilo",
"shuttingDownApp": "Spegnimento...",
"clickableLinksCopy": "Copiare l'URL",
"clickableLinkOpen": "Aprire l'URL",
"formattingExperiment": "Formattazione dei messaggi",
"localeDa": "Danese",
"localeCy": "Gallese",
"settingTheme": "Usa Temi Leggeri",
@ -80,10 +99,6 @@
"btnSendFile": "Invia File",
"newMessagesLabel": "Nuovi Messaggi",
"groupNameLabel": "Nome del Gruppo",
"titleManageServers": "Gestisci i Server",
"leaveConversation": "Lascia Questa Conversazione",
"yesLeave": "Sì, Lascia Questa Conversazione",
"newPassword": "Nuova Password",
"sendMessage": "Invia Messaggio",
"tooltipShowPassword": "Mostra la Password",
"tooltipHidePassword": "Nascondi la Password",
@ -97,22 +112,12 @@
"settingServers": "Server di Hosting",
"openFolderButton": "Apri Cartella",
"reallyLeaveThisGroupPrompt": "Confermi di voler lasciare questa conversazione? Tutti i messaggi e gli attributi verranno eliminati.",
"titleManageProfiles": "Gestisci i Profili Cwtch",
"enableGroups": "Abilita la Chat di Gruppo",
"addListItem": "Aggiungi un Nuovo Elemento alla Lista",
"newConnectionPaneTitle": "Nuova Connessione",
"viewGroupMembershipTooltip": "Visualizza i Membri del Gruppo",
"yourServers": "I Tuoi Server",
"yourProfiles": "I Tuoi Profili",
"deleteProfileConfirmBtn": "Elimina Definitivamente il Profilo",
"deleteProfileBtn": "Elimina Profilo",
"saveProfileBtn": "Salva il Profilo",
"createProfileBtn": "Crea un Profilo",
"currentPasswordLabel": "Password Corrente",
"yourDisplayName": "Il tuo Nome Visualizzato",
"newProfile": "Nuovo Profilo",
"editProfileTitle": "Modifica Profilo",
"puzzleGameBtn": "Gioco di Puzzle",
"searchList": "Elenco di Ricerca",
"dmTooltip": "Clicca per inviare un Messaggio Diretto",
"viewServerInfo": "Informazioni sul Server",

View File

@ -1,6 +1,11 @@
{
"@@locale": "lb",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,6 +1,11 @@
{
"@@locale": "no",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,19 +1,24 @@
{
"@@locale": "pl",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",
"clickableLinksCopy": "Copy URL",
"clickableLinkOpen": "Open URL",
"clickableLinksWarning": "Opening this URL will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open URLs from people you trust. Are you sure you want to continue?",
"shuttingDownApp": "Shutting down...",
"successfullyImportedProfile": "Successfully Imported Profile: %profile",
"failedToImportProfile": "Error Importing Profile",
"importProfileTooltip": "Use an encrypted Cwtch backup to bring in a profile created in another instance of Cwtch.",
"importProfile": "Import Profile",
"exportProfileTooltip": "Backup this profile to an encrypted file. The encrypted file can be imported into another Cwtch app.",
"exportProfile": "Export Profile",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Nie udało się ponownie włączyć optymalizacji użycia baterii dla Cwtch. Przejdź do Android \/ Ustawienia \/ Aplikacje \/ Cwtch \/ Bateria i ustaw Zużycie na 'Optymalizacja'",
"settingAndroidPowerExemptionDescription": "Opcjonalne: wyłącz optymalizację użycia baterii przez Cwtch w systemie Android. Będzie to skutkować lepszą stabilnością w zamian za wyższy pobór energii",
"settingAndroidPowerExemption": "Ignoruj optymalizację użycia baterii przez Cwtch (Android)",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Ta funkcja wymaga włączenia w Ustawieniach funkcji eksperymentalnej: Grupy",
"messageFormattingDescription": "Włącz formatowanie tekstu w wyświetlanych wiadomościach, np. **pogrubiony** and *kursywa*",
"formattingExperiment": "Formatowanie wiadomości",
"clickableLinkError": "Nie udało się otworzyć linku",
"clickableLinksCopy": "Kopiuj link",
"clickableLinkOpen": "Otwórz link",
"clickableLinksWarning": "Otwarcie tego linku spowoduje uruchomienie aplikacji poza Cwtch i może ujawnić metadane lub w inny sposób obniżyć bezpieczeństwo Cwtch. Otwieraj tylko linki otrzymane od zaufanych osób. Czy na pewno chcesz kontynuować? ",
"shuttingDownApp": "Zamykanie...",
"successfullyImportedProfile": "Pomyślnie zaimportowano profil: %profile",
"failedToImportProfile": "Importowanie profilu nie powiodło się",
"importProfileTooltip": "Użyj zaszyfrowanej kopii zapasowej Cwtch aby zaimportować profil utworzony w Cwtch na innym urządzeniu.",
"importProfile": "Importuj profil",
"exportProfileTooltip": "Utwórz zaszyfrowany plik z kopią zapasową tego profilu. Zaszyfrowany plik można zaimportować do Cwtch na innym urządzeniu.",
"exportProfile": "Eksportuj profil",
"deleteConfirmLabel": "Wpisz USUŃ aby potwierdzić",
"localeLb": "Luksemburski",
"localeNo": "Norweski",
@ -21,42 +26,42 @@
"localeCy": "Walijski",
"localeDa": "Duński",
"localeRo": "Romanian",
"newMessageNotificationConversationInfo": "New Message From %1",
"newMessageNotificationSimple": "New Message",
"notificationContentContactInfo": "Conversation Information",
"notificationContentSimpleEvent": "Plain Event",
"conversationNotificationPolicySettingDescription": "Control notification behaviour for this conversation",
"conversationNotificationPolicySettingLabel": "Conversation Notification Policy",
"settingsGroupExperiments": "Experiments",
"settingsGroupAppearance": "Appearance",
"settingGroupBehaviour": "Behaviour",
"notificationContentSettingDescription": "Controls the contents of conversation notifications",
"notificationPolicySettingDescription": "Controls the default application notification behaviour",
"notificationContentSettingLabel": "Notification Content",
"notificationPolicySettingLabel": "Notification Policy",
"conversationNotificationPolicyNever": "Never",
"conversationNotificationPolicyOptIn": "Opt In",
"conversationNotificationPolicyDefault": "Default",
"notificationPolicyDefaultAll": "Default All",
"notificationPolicyOptIn": "Opt In",
"notificationPolicyMute": "Mute",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",
"fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.",
"fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.",
"torSettingsErrorSettingPort": "Port Number must be between 1 and 65535",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.",
"torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)",
"torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy",
"torSettingsCustomControlPort": "Custom Control Port",
"torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy",
"torSettingsCustomSocksPort": "Custom SOCKS Port",
"torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service",
"torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration",
"newMessageNotificationConversationInfo": "Nowa wiadomość od %1",
"newMessageNotificationSimple": "Nowa wiadomość",
"notificationContentContactInfo": "Informacje o konwersacji",
"notificationContentSimpleEvent": "Bez zawartości",
"conversationNotificationPolicySettingDescription": "Zmień zachowanie powiadomień dla tej konwersacji",
"conversationNotificationPolicySettingLabel": "Wyświetlanie powiadomień dla tej konwersacji",
"settingsGroupExperiments": "Funkcje eksperymentalne",
"settingsGroupAppearance": "Wygląd",
"settingGroupBehaviour": "Zachowanie",
"notificationContentSettingDescription": "Zmienia zawartość powiadomień dla konwersacji",
"notificationPolicySettingDescription": "Zarządza domyślnymi ustawieniami wyświetlania powiadomień",
"notificationContentSettingLabel": "Zawartość powiadomień",
"notificationPolicySettingLabel": "Wyświetlanie powiadomień",
"conversationNotificationPolicyNever": "Nie",
"conversationNotificationPolicyOptIn": "Tak",
"conversationNotificationPolicyDefault": "Domyślne",
"notificationPolicyDefaultAll": "Dla wszystkich konwersacji (domyślne)",
"notificationPolicyOptIn": "Tylko dla wybranych konwersacji",
"notificationPolicyMute": "Wycisz",
"tooltipSelectACustomProfileImage": "Ustaw zdjęcie profilowe",
"torSettingsEnabledCacheDescription": "Zapamiętaj obecny konsensus Tor, aby użyć go przy następnym uruchomieniu Cwtch. Dzięki temu Tor uruchomi się szybciej. Jeśli opcja jest wyłączona, Cwtch usuwa zapisany konsensus przy uruchomieniu.",
"torSettingsEnableCache": "Zapamiętaj konsensus Tor",
"labelTorNetwork": "Sieć Tor",
"descriptionACNCircuitInfo": "Szczegółowe informacje na temat trasy wykorzystywanej przez anonimową sieć komunikacji, aby połączyć się z tą konwersacją.",
"labelACNCircuitInfo": "Informacje o trasie ACN",
"fileSharingSettingsDownloadFolderTooltip": "Przeglądaj, aby wybrać inny folder dla pobranych plików.",
"fileSharingSettingsDownloadFolderDescription": "Kiedy pliki są pobierane automatycznie (np. zdjęcia, kiedy opcja podglądu jest włączona), potrzebny jest folder dla pobranych plików.",
"torSettingsErrorSettingPort": "Numer portu musi być między 1 a 65535",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Nadpisz domyślną konfigurację Tor. Uwaga: To może być niebezpieczne. Włącz tę funkcję tylko jeśli wiesz, co robisz.",
"torSettingsUseCustomTorServiceConfiguration": "Użyj niestandardowej konfiguracji Tor (torrc)",
"torSettingsCustomControlPortDescription": "Wybierz port dla połączeń kontrolnych z Tor proxy",
"torSettingsCustomControlPort": "Port kontrolny",
"torSettingsCustomSocksPortDescription": "Wybierz port dla połączeń przekazujących dane do Tor proxy",
"torSettingsCustomSocksPort": "Port SOCKS",
"torSettingsEnabledAdvancedDescription": "Użyj obecnego na twoim systemie serwisu Tor lub zmień parametry serwisu Tor w Cwtch",
"torSettingsEnabledAdvanced": "Włącz zaawansowaną konfigurację Tor",
"largeTextLabel": "Duży",
"settingInterfaceZoom": "Przybliżenie",
"localeDe": "Deutsche",
@ -87,7 +92,7 @@
"password1Label": "Hasło",
"currentPasswordLabel": "Obecne hasło",
"yourDisplayName": "Nazwa",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"profileOnionLabel": "Przekaż ten adres osobom, z którymi chcesz nawiązać kontakt",
"noPasswordWarning": "Brak hasła do konta oznacza, że dane przechowywane na tym urządzeniu nie będą zaszyfrowane",
"radioNoPassword": "Niezaszyfrowany (brak hasła)",
"radioUsePassword": "Hasło",
@ -97,13 +102,13 @@
"profileName": "Nazwa",
"editProfileTitle": "Edytuj profil",
"addProfileTitle": "Dodaj nowy profil",
"deleteBtn": "Delete",
"deleteBtn": "Usuń",
"unblockBtn": "Odblokuj",
"dontSavePeerHistory": "Nie",
"savePeerHistoryDescription": "Zapisywanie wiadomości",
"savePeerHistory": "Tak",
"blockBtn": "Zablokuj",
"saveBtn": "Save",
"saveBtn": "Zapisz",
"displayNameLabel": "Nazwa",
"addressLabel": "Adresy",
"puzzleGameBtn": "Puzzle",
@ -124,7 +129,7 @@
"membershipDescription": "Lista użytkowników, którzy wysyłali wiadomości w tej grupie. Członkowie grupy, którzy nie wysyłali żadnych wiadomości nie są na tej liście.",
"addListItemBtn": "Dodaj",
"peerNotOnline": "Znajomy jest niedostępny. Nie można użyć aplikacji.",
"searchList": "Search List",
"searchList": "Lista wyszukiwania",
"update": "Zaktualizuj",
"inviteBtn": "Zaproś",
"inviteToGroupLabel": "Zaproś do grupy",
@ -220,7 +225,7 @@
"tooltipRejectContactRequest": "Odrzuć zaproszenie do znajomych",
"tooltipAcceptContactRequest": "Akceptuj zaproszenie do znajomych",
"notificationNewMessageFromPeer": "Nowa wiadomość od znajomego!",
"groupInviteSettingsWarning": "Zaproszono Cię do grupy! Aby wyświetlić to zaproszenie, włącz Czaty Grupowe (eksperymentalne) w Ustawieniach",
"groupInviteSettingsWarning": "Zaproszono Cię do grupy! Aby wyświetlić to zaproszenie, włącz Grupy (eksperymentalne) w Ustawieniach",
"shutdownCwtchDialog": "Zamknąć Cwtch? Wszystkie połączenia zostaną zakończone, a aplikacja zostanie zamknięta.",
"malformedMessage": "Wiadomość uszkodzona",
"profileDeleteSuccess": "Profil został usunięty",
@ -243,21 +248,21 @@
"inviteToGroup": "Zaproszono Cię do grupy:",
"successfullAddedContact": "Dodano znajomego ",
"descriptionBlockUnknownConnections": "Blokowanie połączeń od osób, które nie są na liście Twoich znajomych.",
"descriptionExperimentsGroups": "Czaty grupowe (eksperymentalne) łączą się z niezaufanymi serwerami, aby umożliwić komunikację grupową.",
"descriptionExperiments": "Funkcje eksperymentalne są opcjonalne. Dodają one funkcjonalności, które mogą być mniej prywatne niż domyślne konwersacje 1:1, np. czaty grupowe, integracja z botami, itp.",
"descriptionExperimentsGroups": "Grupy (eksperymentalne) łączą się z niezaufanymi serwerami, aby umożliwić komunikację grupową.",
"descriptionExperiments": "Funkcje eksperymentalne są opcjonalne. Dodają one funkcjonalności, które mogą być mniej prywatne niż domyślne konwersacje 1:1, np. Grupy, integracja z botami, itp.",
"titleManageProfiles": "Zarządzaj Profilami",
"tooltipUnlockProfiles": "Wprowadź hasło, aby odblokować zaszyfrowane profile.",
"titleManageContacts": "Konwersacje",
"tooltipAddContact": "Dodaj znajomego lub grupę",
"tooltipOpenSettings": "Ustawienia",
"contactAlreadyExists": "Ten znajomy już istnieje",
"invalidImportString": "Invalid import string",
"invalidImportString": "Niepoprawny ciąg importu",
"conversationSettings": "Ustawienia konwersacji",
"enterCurrentPasswordForDelete": "Aby usunąć ten profil, wprowadź hasło.",
"enableGroups": "Włącz czaty grupowe",
"enableGroups": "Włącz Grupy",
"localeIt": "Italiana",
"localeEs": "Espanol",
"todoPlaceholder": "Do zdobienia...",
"todoPlaceholder": "Do zrobienia...",
"addNewItem": "Dodaj do listy",
"addListItem": "Add a New List Item",
"newConnectionPaneTitle": "Nowe połączenie",
@ -316,7 +321,7 @@
"fileSavedTo": "Zapisano do",
"verfiyResumeButton": "Zweryfikuj\/wznów",
"copyServerKeys": "Kopiuj klucze",
"archiveConversation": "Zarchiwizuj tę rozmowę",
"archiveConversation": "Zarchiwizuj tę konwersację",
"streamerModeLabel": "Tryb streamera\/prezentacji",
"retrievingManifestMessage": "Pobieranie informacji o pliku...",
"openFolderButton": "Otwórz folder",
@ -326,5 +331,5 @@
"messageFileSent": "Plik został wysłany",
"tooltipSendFile": "Wyślij plik",
"settingFileSharing": "Udostępnianie plików",
"copiedToClipboardNotification": "Copied to Clipboard"
"copiedToClipboardNotification": "Skopiowano do schowka"
}

View File

@ -1,6 +1,11 @@
{
"@@locale": "pt",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,6 +1,11 @@
{
"@@locale": "ro",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"@@last_modified": "2022-04-21T21:35:58+02:00",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",

View File

@ -1,85 +1,125 @@
{
"@@locale": "ru",
"@@last_modified": "2022-04-06T22:31:33+02:00",
"messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Message Formatting",
"clickableLinkError": "Error encountered while attempting to open URL",
"clickableLinksCopy": "Copy URL",
"clickableLinkOpen": "Open URL",
"clickableLinksWarning": "Opening this URL will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open URLs from people you trust. Are you sure you want to continue?",
"shuttingDownApp": "Shutting down...",
"successfullyImportedProfile": "Successfully Imported Profile: %profile",
"failedToImportProfile": "Error Importing Profile",
"importProfileTooltip": "Use an encrypted Cwtch backup to bring in a profile created in another instance of Cwtch.",
"importProfile": "Import Profile",
"exportProfileTooltip": "Backup this profile to an encrypted file. The encrypted file can be imported into another Cwtch app.",
"exportProfile": "Export Profile",
"@@last_modified": "2022-06-22T00:46:01+02:00",
"localeDe": "Немецкий \/ Deutsch",
"localeDa": "Датский язык \/ Dansk",
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами.",
"localePt": "Португальский язык \/ Portuguesa",
"localeIt": "Итальянский \/ Italiana",
"tooltipBackToMessageEditing": "Назад к редактированию сообщений",
"tooltipItalicize": "Курсив",
"tooltipCode": "Код \/ Монопространство",
"localeEn": "Английский \/ English",
"localePl": "Польский \/ Polski",
"localeNo": "Норвежский \/ Norsk",
"tooltipSubscript": "Подстрочный",
"tooltipBoldText": "Смелый",
"localeCy": "Валлийский \/ Cymraeg",
"tooltipSuperscript": "Надстрочный",
"localeRo": "Румынский \/ Română",
"localeEl": "Греческий \/ Ελληνικά",
"localeLb": "Люксембургский \/ Lëtzebuergesch",
"tooltipPreviewFormatting": "Предварительный просмотр форматирования сообщений",
"tooltipStrikethrough": "Зачеркивание",
"localeFr": "Французский \/ Français",
"localeEs": "Испанский \/ Español",
"localeRU": "Русский \/ Русский",
"editProfile": "Изменить профиль",
"okButton": "OK",
"settingsAndroidPowerReenablePopup": "Невозможно перезапустить функцию оптимазации батарее для Cwtch. Перейдите в настройки Android \/ Настройки \/ Приложения и уведомления \/ Все приложения \/ Cwtch \/ Батарея \/ Эконоимя заряда \/ Отключена",
"settingAndroidPowerExemptionDescription": "Необязательно: в настройках Android исключите Cwtch в параметрах оптимизации батареи. Это улучшит стабильность за счёт небольшого расхода батареи..",
"settingAndroidPowerExemption": "Игнорировать оптимазацию батареи Android",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Чтобы использовать данную функцию, в настройках необходимо включить \"Эксперементы\", затем \"Групповые чаты\"",
"messageFormattingDescription": "Включите форматирование, если к примеру хотите использовать **жирный-текст** и *курсив*",
"formattingExperiment": "Форматирование сообщений",
"clickableLinkError": "Ошибка при попытке открыть данную ссылку",
"clickableLinksCopy": "Копировать ссылку",
"clickableLinkOpen": "Открыть ссылку",
"clickableLinksWarning": "Открытие данной ссылки приведет к запуску приложения за пределами Cwtch и может раскрыть метаданные или иным образом поставить под угрозу безопасность Cwtch. Открывайте ссылки только от тех людей, которым вы доверяете. Вы уверены, что хотите продолжить?",
"shuttingDownApp": "Выключение...",
"successfullyImportedProfile": "Профиль успешно импортирован: %profile",
"failedToImportProfile": "Ошибка импорта профиля",
"importProfileTooltip": "Используйте зашифрованную резервную копию Cwtch, чтобы перенести профиль, созданный на другом устройстве где установлен Cwtch..",
"importProfile": "Загрузить профиль",
"exportProfileTooltip": "Сделать зашифрованную резервную копию в файл. Его потом потом можно импортировать на других устройствах где установлен Cwtch.",
"exportProfile": "Экспорт профиля",
"notificationContentContactInfo": "Показать текст сообщения",
"notificationContentSimpleEvent": "Без подробностей",
"conversationNotificationPolicySettingDescription": "Настройка уведомлений",
"conversationNotificationPolicySettingLabel": "Настройка уведомлений",
"settingsGroupAppearance": "НАСТРОЙКИ ОТОБРАЖЕНИЯ",
"notificationContentSettingDescription": "Управление уведомлениями чатов",
"notificationPolicySettingDescription": "Настройка уведомлений по-умолчанию",
"notificationContentSettingLabel": "Содержимое уведомления",
"notificationPolicySettingLabel": "Уведомления",
"conversationNotificationPolicyOptIn": "Включить",
"conversationNotificationPolicyDefault": "По-умолчанию",
"notificationPolicyDefaultAll": "По-умолчанию",
"notificationPolicyOptIn": "Включить",
"notificationPolicyMute": "Без звука",
"tooltipSelectACustomProfileImage": "Сменить изображение профиля",
"torSettingsEnabledCacheDescription": "Кэшировать текущий загруженный узел Tor для повторного подключения при следующем запуске Cwtch. Это позволит Tor запускаться быстрее. Если этот параметр отключен, Cwtch будет очищать кэшированные данные при запуске.",
"torSettingsEnableCache": "Кешировать узлы Tor",
"descriptionACNCircuitInfo": "Подробная информация о соединении, который сеть анонимной связи использует для подключения к этому разговору.",
"labelACNCircuitInfo": "Информация о цепи ACN",
"fileSharingSettingsDownloadFolderTooltip": "Нажмите обзор чтобы выбрать другую папку по-умолчанию для загружаемых файлов.",
"fileSharingSettingsDownloadFolderDescription": "При включение функции автоматическое скачивание файлов (например картинок), необходимо указать папку для сохранения.",
"torSettingsErrorSettingPort": "Номер порта должен быть в диапазоне от 1 до 65535",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Переопределение конфигурации Tor по умолчанию. Предупреждение: это может быть опасно. Если не знаете, что делаете, лучше не трогать!",
"torSettingsUseCustomTorServiceConfiguration": "Используйте пользовательскую конфигурацию службы Tor (torrc)",
"torSettingsCustomControlPortDescription": "Используйте настраиваемый порт для управления подключениями к прокси-серверу Tor.",
"torSettingsCustomControlPort": "Выберите контрольный порт",
"torSettingsCustomSocksPortDescription": "Используйте настраиваемый порт для подключения к прокси-серверу Tor.",
"torSettingsCustomSocksPort": "Выберите SOCKS порт",
"torSettingsEnabledAdvancedDescription": "Использовать установленную службу Tor в вашей системе или измените параметры службы Cwtch Tor",
"torSettingsEnabledAdvanced": "Включить расширенные настройки Tor",
"themeColorLabel": "Основной цвет темы",
"settingDownloadFolder": "Папка для загрузок",
"importLocalServerLabel": "Использовать локальный сервер",
"deleteServerConfirmBtn": "Вы точно хотите удалить сервер?",
"unlockProfileTip": "Создайте или импортируйте профиль, чтобы начать",
"unlockServerTip": "Создайте или импортируйте сервер, чтобы начать",
"saveServerButton": "Сохранить",
"serverEnabled": "Состояние сервера",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch, внутри сети Tor",
"settingUIColumnOptionSame": "Как в портретном режиме",
"settingUIColumnSingle": "Один столбец",
"createProfileToBegin": "Пожалуйста, создайте или импортируйте профиль, чтобы начать",
"yesLeave": "Удалить",
"leaveConversation": "Удалить",
"enableGroups": "Групповые чаты",
"settingTheme": "Ночной режим",
"addNewProfileBtn": "Создать новый профиль",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"savePeerHistoryDescription": "Определяет политику хранения или удаления сообщений с данным контактом",
"savePeerHistory": "Настройка истории",
"deleteConfirmLabel": "Введите УДАЛИТЬ, чтобы продолжить",
"deleteConfirmText": "УДАЛИТЬ",
"localeCy": "Валлийский",
"localeDa": "Датский",
"localeEl": "Греческий",
"localeNo": "Норвежский",
"localeLb": "Люксембургский",
"settingsGroupAppearance": "Появление",
"settingGroupBehaviour": "Поведение",
"settingsGroupExperiments": "Эксперименты",
"settingGroupBehaviour": "ПОВЕДЕНИЕ",
"settingsGroupExperiments": "ЭКСПЕРИМЕНТЫ",
"labelTorNetwork": "Сеть Tor",
"notificationPolicyMute": "Тишина",
"conversationNotificationPolicyNever": "Никогда",
"conversationNotificationPolicyNever": "Отключить",
"newMessageNotificationConversationInfo": "Новое сообщение от %1",
"newMessageNotificationSimple": "Новое сообщение",
"localeRo": "Румынский",
"notificationContentContactInfo": "Conversation Information",
"notificationContentSimpleEvent": "Plain Event",
"conversationNotificationPolicySettingDescription": "Control notification behaviour for this conversation",
"conversationNotificationPolicySettingLabel": "Conversation Notification Policy",
"notificationContentSettingDescription": "Controls the contents of conversation notifications",
"notificationPolicySettingDescription": "Controls the default application notification behaviour",
"notificationContentSettingLabel": "Notification Content",
"notificationPolicySettingLabel": "Notification Policy",
"conversationNotificationPolicyOptIn": "Opt In",
"conversationNotificationPolicyDefault": "Default",
"notificationPolicyDefaultAll": "Default All",
"notificationPolicyOptIn": "Opt In",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",
"fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.",
"fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.",
"torSettingsErrorSettingPort": "Port Number must be between 1 and 65535",
"msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.",
"btnSendFile": "Отправить файл",
"msgConfirmSend": "Вы уверены, что хотите отправить?",
"msgFileTooBig": "Размер файла не должен превышать 10GB",
"storageMigrationModalMessage": "Перенос профилей в новый формат хранения. Это может занять несколько минут...",
"loadingCwtch": "Загрузка Cwtch...",
"themeColorLabel": "Светлая или Темная тема",
"settingDownloadFolder": "Папка для скачивания",
"serverConnectionsLabel": "Всего соединений:",
"serverTotalMessagesLabel": "Всего сообщений:",
"plainServerDescription": "Мы настоятельно рекомендуем защитить свой Onion сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.",
"settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер для Cwtch в скрытой сети Tor. В меню появится дополнительная опция Серверы",
"streamerModeLabel": "Режим маскировки",
"settingUIColumnSingle": "Один стобец",
"settingUIColumnLandscape": "Столбцы чатов в ландшафтном режиме",
"settingUIColumnPortrait": "Столбцы чатов в портретном режиме",
"resetTor": "Сброс",
"descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей, не состоящих в ваших контактах будут отклонены.",
"descriptionExperimentsGroups": "Данная экспериментальная функция позволяет создавать группы в Cwtch, чтобы облегчить Вам общение с более чем одним контактом. Для создания групп необходимо включить функцию создания сервера и создать сервер в главном меню программы.",
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1..",
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1.",
"settingLanguage": "Выбрать язык",
"profileName": "Введите имя...",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.",
"torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)",
"torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy",
"torSettingsCustomControlPort": "Custom Control Port",
"torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy",
"torSettingsCustomSocksPort": "Custom SOCKS Port",
"torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service",
"torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration",
"themeNameNeon2": "Неон2",
"themeNameNeon1": "Неон1",
"themeNameMidnight": "Полночь",
@ -89,7 +129,6 @@
"themeNameVampire": "Вампир",
"themeNameWitch": "Ведьма",
"themeNameCwtch": "Cwtch",
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами. Аватары профиля запланированы для версии Cwtch 1.6.",
"settingImagePreviews": "Предпросмотр изображений и фотографий профиля",
"experimentClickableLinksDescription": "Экспериментальная функция которая позволяет нажимать на URL адреса в сообщениях",
"enableExperimentClickableLinks": "Включить кликабельные ссылки",
@ -102,11 +141,7 @@
"groupsOnThisServerLabel": "Группы, в которых я нахожусь, размещены на этом сервере",
"importLocalServerButton": "Импорт %1",
"importLocalServerSelectText": "Выбрать локальный сервер",
"importLocalServerLabel": "Импортировать локальный сервер",
"newMessagesLabel": "Новое сообщение",
"localeRU": "Русский",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"savePeerHistory": "Хранить историю",
"saveBtn": "Сохранить",
"networkStatusOnline": "В сети",
"defaultProfileName": "Алиса",
@ -122,29 +157,23 @@
"fileInterrupted": "Прервано",
"fileSavedTo": "Сохранить в",
"encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
"deleteServerConfirmBtn": "Точно удалить сервер?",
"deleteServerSuccess": "Сервер успешно удален",
"enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его",
"copyAddress": "Копировать адрес",
"settingServers": "Использовать серверы",
"enterServerPassword": "Введите пароль для разблокировки сервера",
"unlockProfileTip": "Создайте или разблокируйте профиль, чтобы начать",
"unlockServerTip": "Создайте или разблокируйте сервер, чтобы начать",
"addServerTooltip": "Добавить сервер",
"serversManagerTitleShort": "Серверы",
"serversManagerTitleLong": "Личные серверы",
"saveServerButton": "Сохранить сервер",
"serverAutostartDescription": "Автозапуск сервера при старте программы",
"serverAutostartLabel": "Автозапуск",
"serverEnabledDescription": "Запустить или остановить сервер",
"serverEnabled": "Сервер запущен",
"serverDescriptionDescription": "Описание видите только Вы. Сделано для удобства",
"serverDescriptionLabel": "Описание сервера",
"serverAddress": "Адрес сервера",
"editServerTitle": "Изменить сервер",
"addServerTitle": "Добавить сервер",
"titleManageProfilesShort": "Профили",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.",
"settingFileSharing": "Передача файлов",
"tooltipSendFile": "Отправить файл",
"messageFileOffered": "Контакт предлагает загрузить вам файл",
@ -166,10 +195,8 @@
"addContactConfirm": "Добавить контакт %1",
"addContact": "Добавить контакт",
"contactGoto": "Перейти к сообщению от %1",
"settingUIColumnOptionSame": "Как в настройках портретного режима",
"settingUIColumnDouble14Ratio": "Двойной (1:4)",
"settingUIColumnDouble12Ratio": "Двойной (1:2)",
"localePl": "Польский",
"tooltipRemoveThisQuotedMessage": "Удалить цитируемое сообщение.",
"tooltipReplyToThisMessage": "Ответить на это сообщение",
"tooltipRejectContactRequest": "Отклонить запрос в контакты.",
@ -188,7 +215,6 @@
"debugLog": "Влючить отладку через консоль",
"torNetworkStatus": "Статус сети Tor",
"addContactFirst": "Добавьте или выберите контакт, чтобы начать чат.",
"createProfileToBegin": "Пожалуйста, создайте или разблокируйте профиль, чтобы начать",
"nickChangeSuccess": "Имя профиля успешно изменено",
"addServerFirst": "Перед созданием группы, необходимо создать сервер",
"deleteProfileSuccess": "Профиль успешно удален",
@ -203,9 +229,7 @@
"accepted": "Принять!",
"chatHistoryDefault": "Этот чат будет удален после закрытия Cwtch! Историю сообщений можно включить для каждого чата отдельно через меню настроек в правом верхнем углу..",
"newPassword": "Новый пароль",
"yesLeave": "Да, оставить этот чат",
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
"leaveConversation": "Да, оставить этот чат",
"inviteToGroup": "Вас пригласили присоединиться к группе:",
"titleManageServers": "Управление серверами",
"successfullAddedContact": "Успешно добавлен",
@ -218,9 +242,6 @@
"invalidImportString": "Недействительная строка импорта",
"conversationSettings": "Настройки чата",
"enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.",
"enableGroups": "Включить Групповые чаты",
"localeIt": "Итальянский",
"localeEs": "Испанский",
"todoPlaceholder": "Выполняю...",
"addNewItem": "Добавить новый элемент в список",
"addListItem": "Добавить новый элемент",
@ -238,13 +259,8 @@
"experimentsEnabled": "Включить Экспериментальные функции",
"themeDark": "Темная",
"themeLight": "Светлая",
"settingTheme": "Тема",
"largeTextLabel": "Большой",
"settingInterfaceZoom": "Уровень масштабирования",
"localeDe": "Немецкий",
"localePt": "Португальский",
"localeFr": "Французский",
"localeEn": "Английский",
"blockUnknownLabel": "Блокировать неизвестные контакты",
"zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)",
"versionBuilddate": "Версия: %1 Сборка от: %2",
@ -255,7 +271,6 @@
"error0ProfilesLoadedForPassword": "0 профилей, загруженных с этим паролем",
"password": "Пароль",
"enterProfilePassword": "Введите пароль для просмотра ваших профилей",
"addNewProfileBtn": "Добавить новый профиль",
"deleteProfileConfirmBtn": "Действительно удалить профиль?",
"deleteProfileBtn": "Удалить профиль",
"passwordChangeError": "Ошибка при смене пароля: Введенный пароль отклонен",
@ -270,13 +285,11 @@
"noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы",
"radioNoPassword": "Незашифрованный (без пароля)",
"radioUsePassword": "Пароль",
"editProfile": "Изменить профиль",
"newProfile": "Новый профиль",
"editProfileTitle": "Изменить профиль",
"addProfileTitle": "Добавить новый профиль",
"unblockBtn": "Разблокировать контакт",
"dontSavePeerHistory": "Удалить историю",
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
"blockBtn": "Заблокировать контакт",
"displayNameLabel": "Отображаемое имя",
"addressLabel": "Адрес",

View File

@ -343,5 +343,4 @@ SOFTWARE.''');
See the License for the specific language governing permissions and
limitations under the License.
''');
}

View File

@ -1,6 +1,7 @@
import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'message.dart';
import 'messagecache.dart';
@ -42,9 +43,9 @@ class ContactInfoState extends ChangeNotifier {
late int _totalMessages = 0;
late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageRowState>> keys;
int _newMarkerMsgId = -1;
DateTime _newMarkerClearAt = DateTime.now();
int _newMarkerMsgIndex = -1;
late MessageCache messageCache;
ItemScrollController messageScrollController = new ItemScrollController();
// todo: a nicer way to model contacts, groups and other "entities"
late bool _isGroup;
@ -145,25 +146,24 @@ class ContactInfoState extends ChangeNotifier {
notifyListeners();
}
void selected() {
this._newMarkerMsgIndex = this._unreadMessages - 1;
this._unreadMessages = 0;
}
void unselected() {
this._newMarkerMsgIndex = -1;
}
int get unreadMessages => this._unreadMessages;
set unreadMessages(int newVal) {
if (newVal == 0) {
// conversation has been selected, start the countdown for the New Messager marker to be reset
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2));
}
this._unreadMessages = newVal;
notifyListeners();
}
int get newMarkerMsgId {
if (DateTime.now().isAfter(this._newMarkerClearAt)) {
// perform heresy
this._newMarkerMsgId = -1;
// no need to notifyListeners() because presumably this getter is
// being called from a renderer anyway
}
return this._newMarkerMsgId;
int get newMarkerMsgIndex {
return this._newMarkerMsgIndex;
}
int get totalMessages => this._totalMessages;
@ -243,8 +243,12 @@ class ContactInfoState extends ChangeNotifier {
if (!selectedConversation) {
unreadMessages++;
}
if (_newMarkerMsgId == -1) {
_newMarkerMsgId = messageID;
if (_newMarkerMsgIndex == -1) {
if (!selectedConversation) {
_newMarkerMsgIndex = 0;
}
} else {
_newMarkerMsgIndex++;
}
this._lastMessageTime = timestamp;

View File

@ -127,4 +127,8 @@ class ContactListState extends ChangeNotifier {
getContact(identifier)?.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation);
updateLastMessageTime(identifier, DateTime.now());
}
int cacheMemUsage() {
return _contacts.map((e) => e.messageCache.size()).fold(0, (previousValue, element) => previousValue + element);
}
}

View File

@ -32,33 +32,33 @@ const GroupConversationHandleLength = 32;
abstract class Message {
MessageMetadata getMetadata();
Widget getWidget(BuildContext context, Key key);
Widget getWidget(BuildContext context, Key key, int index);
Widget getPreviewWidget(BuildContext context);
}
Message compileOverlay(MessageMetadata metadata, String messageData) {
Message compileOverlay(MessageInfo messageInfo) {
try {
dynamic message = jsonDecode(messageData);
dynamic message = jsonDecode(messageInfo.wrapper);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
return TextMessage(messageInfo.metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
return InviteMessage(overlay, messageInfo.metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
return QuotedMessage(messageInfo.metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
return FileMessage(messageInfo.metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
return MalformedMessage(messageInfo.metadata);
}
} catch (e) {
return MalformedMessage(metadata);
return MalformedMessage(messageInfo.metadata);
}
}
@ -77,41 +77,67 @@ class ByIndex implements CacheHandler {
}
Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
// if in cache, get
// if in cache, get. But if the cache has unsynced or not in cache, we'll have to do a fetch
if (index < cache.cacheByIndex.length) {
return cache.getByIndex(index);
}
// otherwise we are going to fetch, so we'll fetch a chunk of messages
// observationally flutter future builder seemed to be reaching for 20-40 message on pane load, so we start trying to load up to that many messages in one request
var chunk = 40;
var amount = 40;
var start = index;
// we have to keep the indexed cache contiguous so reach back to the end of it and start the fetch from there
if (index > cache.cacheByIndex.length) {
start = cache.cacheByIndex.length;
amount += index - start;
}
// check that we aren't asking for messages beyond stored messages
if (index + chunk >= cache.storageMessageCount) {
chunk = cache.storageMessageCount - index;
if (chunk <= 0) {
if (start + amount >= cache.storageMessageCount) {
amount = cache.storageMessageCount - start;
if (amount <= 0) {
return Future.value(null);
}
}
cache.lockIndexes(index, index + chunk);
var msgs = await cwtch.GetMessages(profileOnion, conversationIdentifier, index, chunk);
cache.lockIndexes(start, start + amount);
await fetchAndProcess(start, amount, cwtch, profileOnion, conversationIdentifier, cache);
return cache.getByIndex(index);
}
void loadUnsynced(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) {
// return if inadvertently called when no unsynced messages
if (cache.indexUnsynced == 0) {
return;
}
// otherwise we are going to fetch, so we'll fetch a chunk of messages
var start = 0;
var amount = cache.indexUnsynced;
cache.lockIndexes(start, start + amount);
fetchAndProcess(start, amount, cwtch, profileOnion, conversationIdentifier, cache);
return;
}
Future<void> fetchAndProcess(int start, int amount, Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
var msgs = await cwtch.GetMessages(profileOnion, conversationIdentifier, start, amount);
int i = 0; // i used to loop through returned messages. if doesn't reach the requested count, we will use it in the finally stanza to error out the remaining asked for messages in the cache
try {
List<dynamic> messagesWrapper = jsonDecode(msgs);
for (; i < messagesWrapper.length; i++) {
var messageInfo = messageWrapperToInfo(profileOnion, conversationIdentifier, messagesWrapper[i]);
cache.addIndexed(messageInfo, index + i);
cache.addIndexed(messageInfo, start + i);
}
//messageWrapperToInfo
} catch (e, stacktrace) {
EnvironmentConfig.debugLog("Error: Getting indexed messages $index to ${index + chunk} failed parsing: " + e.toString() + " " + stacktrace.toString());
EnvironmentConfig.debugLog("Error: Getting indexed messages $start to ${start + amount} failed parsing: " + e.toString() + " " + stacktrace.toString());
} finally {
if (i != chunk) {
cache.malformIndexes(index + i, index + chunk);
if (i != amount) {
cache.malformIndexes(start + i, start + amount);
}
}
return cache.getByIndex(index);
}
void add(MessageCache cache, MessageInfo messageInfo) {
@ -195,7 +221,7 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, int co
MessageInfo? messageInfo = await cacheHandler.get(cwtch, profileOnion, conversationIdentifier, cache);
if (messageInfo != null) {
return compileOverlay(messageInfo.metadata, messageInfo.wrapper);
return compileOverlay(messageInfo);
} else {
return MalformedMessage(malformedMetadata);
}

View File

@ -1,14 +1,24 @@
import 'dart:async';
import 'dart:ffi';
import 'package:flutter/foundation.dart';
import 'message.dart';
// we only count up to 100 unread messages, if more than that we can't accurately resync message cache, just reset
// https://git.openprivacy.ca/cwtch.im/libcwtch-go/src/branch/trunk/utils/eventHandler.go#L210
const MaxUnreadBeforeCacheReset = 100;
class MessageInfo {
late MessageMetadata metadata;
late String wrapper;
MessageInfo(this.metadata, this.wrapper);
int size() {
var wrapperSize = wrapper.length * 2;
return wrapperSize;
}
}
class LocalIndexMessage {
@ -68,6 +78,8 @@ class MessageCache extends ChangeNotifier {
// local index to MessageId
late List<LocalIndexMessage> cacheByIndex;
// index unsynced is used on android on reconnect to tell us new messages are in the backend that should be at the front of the index cache
int _indexUnsynced = 0;
// map of content hash to MessageId
late Map<String, int> cacheByHash;
@ -81,13 +93,18 @@ class MessageCache extends ChangeNotifier {
this._storageMessageCount = storageMessageCount;
}
int get indexedLength => cacheByIndex.length;
int get storageMessageCount => _storageMessageCount;
set storageMessageCount(int newval) {
this._storageMessageCount = newval;
}
// On android reconnect, if backend supplied message count > UI message count, add the differnce to the front of the index
void addFrontIndexGap(int count) {
this._indexUnsynced = count;
}
int get indexUnsynced => _indexUnsynced;
MessageInfo? getById(int id) => cache[id];
Future<MessageInfo?> getByIndex(int index) async {
@ -109,7 +126,6 @@ class MessageCache extends ChangeNotifier {
if (contenthash != null && contenthash != "") {
this.cacheByHash[contenthash] = messageID;
}
notifyListeners();
}
// inserts place holder values into the index cache that will block on .get() until .finishLoad() is called on them with message contents
@ -118,6 +134,11 @@ class MessageCache extends ChangeNotifier {
void lockIndexes(int start, int end) {
for (var i = start; i < end; i++) {
this.cacheByIndex.insert(i, LocalIndexMessage(null, isLoading: true));
// if there are unsynced messages on the index cache it means there are messages at the front, and by the logic in message/ByIndex/get() we will be loading those
// there for we can decrement the count as this will be one of them
if (this._indexUnsynced > 0) {
this._indexUnsynced--;
}
}
}
@ -135,7 +156,6 @@ class MessageCache extends ChangeNotifier {
this.cacheByIndex.insert(index, LocalIndexMessage(messageInfo.metadata.messageID));
}
this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
notifyListeners();
}
void addUnindexed(MessageInfo messageInfo) {
@ -143,7 +163,6 @@ class MessageCache extends ChangeNotifier {
if (messageInfo.metadata.contenthash != "") {
this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
}
notifyListeners();
}
void ackCache(int messageID) {
@ -155,4 +174,11 @@ class MessageCache extends ChangeNotifier {
cache[messageID]?.metadata.error = true;
notifyListeners();
}
int size() {
// very naive cache size, assuming MessageInfo are fairly large on average
// and everything else is small in comparison
int cacheSize = cache.entries.map((e) => e.value.size()).fold(0, (previousValue, element) => previousValue + element);
return cacheSize + cacheByHash.length * 64 + cacheByIndex.length * 16;
}
}

View File

@ -18,13 +18,13 @@ class FileMessage extends Message {
FileMessage(this.metadata, this.content);
@override
Widget getWidget(BuildContext context, Key key) {
Widget getWidget(BuildContext context, Key key, int index) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
dynamic shareObj = jsonDecode(this.content);
if (shareObj == null) {
return MessageRow(MalformedBubble());
return MessageRow(MalformedBubble(), index);
}
String nameSuggestion = shareObj['f'] as String;
String rootHash = shareObj['h'] as String;
@ -39,10 +39,10 @@ class FileMessage extends Message {
}
if (!validHash(rootHash, nonce)) {
return MessageRow(MalformedBubble());
return MessageRow(MalformedBubble(), index);
}
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize, isAuto: metadata.isAuto), key: key);
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize, isAuto: metadata.isAuto), index, key: key);
});
}
@ -53,17 +53,19 @@ class FileMessage extends Message {
builder: (bcontext, child) {
dynamic shareObj = jsonDecode(this.content);
if (shareObj == null) {
return MessageRow(MalformedBubble());
return MessageRow(MalformedBubble(), 0);
}
String nameSuggestion = shareObj['n'] as String;
String rootHash = shareObj['h'] as String;
String nonce = shareObj['n'] as String;
int fileSize = shareObj['s'] as int;
if (!validHash(rootHash, nonce)) {
return MessageRow(MalformedBubble());
return MessageRow(MalformedBubble(), 0);
}
return Container(
alignment: Alignment.center,
width: 50,
height: 50,
child: FileBubble(
nameSuggestion,
rootHash,
@ -71,6 +73,7 @@ class FileMessage extends Message {
fileSize,
isAuto: metadata.isAuto,
interactive: false,
isPreview: true,
));
});
}

View File

@ -17,7 +17,7 @@ class InviteMessage extends Message {
InviteMessage(this.overlay, this.metadata, this.content);
@override
Widget getWidget(BuildContext context, Key key) {
Widget getWidget(BuildContext context, Key key, int index) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
@ -36,10 +36,10 @@ class InviteMessage extends Message {
inviteTarget = jsonObj['GroupID'];
inviteNick = jsonObj['GroupName'];
} else {
return MessageRow(MalformedBubble());
return MessageRow(MalformedBubble(), index);
}
}
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: key);
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), index, key: key);
});
}

View File

@ -9,11 +9,11 @@ class MalformedMessage extends Message {
MalformedMessage(this.metadata);
@override
Widget getWidget(BuildContext context, Key key) {
Widget getWidget(BuildContext context, Key key, int index) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (context, child) {
return MessageRow(MalformedBubble(), key: key);
return MessageRow(MalformedBubble(), index, key: key);
});
}

View File

@ -1,17 +1,12 @@
import 'dart:convert';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagerow.dart';
import 'package:cwtch/widgets/quotedmessage.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../../main.dart';
import '../messagecache.dart';
import '../profile.dart';
class QuotedMessageStructure {
final String quotedHash;
final String body;
@ -34,8 +29,14 @@ class QuotedMessage extends Message {
value: this.metadata,
builder: (bcontext, child) {
try {
dynamic message = jsonDecode(this.content);
return Text(message["body"]);
dynamic message = jsonDecode(
this.content,
);
var content = message["body"];
return Text(
content,
overflow: TextOverflow.ellipsis,
);
} catch (e) {
return MalformedBubble();
}
@ -48,7 +49,7 @@ class QuotedMessage extends Message {
}
@override
Widget getWidget(BuildContext context, Key key) {
Widget getWidget(BuildContext context, Key key, int index) {
try {
dynamic message = jsonDecode(this.content);
@ -59,7 +60,8 @@ class QuotedMessage extends Message {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
return MessageRow(QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, ByContentHash(message["quotedHash"]))), key: key);
return MessageRow(QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, ByContentHash(message["quotedHash"]))), index,
key: key);
});
} catch (e) {
return MalformedBubble();

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
@ -19,7 +21,10 @@ class TextMessage extends Message {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
return Text(this.content);
return Text(
this.content,
overflow: TextOverflow.ellipsis,
);
});
}
@ -29,12 +34,13 @@ class TextMessage extends Message {
}
@override
Widget getWidget(BuildContext context, Key key) {
Widget getWidget(BuildContext context, Key key, int index) {
return ChangeNotifierProvider.value(
value: this.metadata,
builder: (bcontext, child) {
return MessageRow(
MessageBubble(this.content),
index,
key: key,
);
});

View File

@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import 'contact.dart';
import 'contactlist.dart';
import 'filedownloadprogress.dart';
import 'messagecache.dart';
import 'profileservers.dart';
class ProfileInfoState extends ChangeNotifier {
@ -175,7 +176,12 @@ class ProfileInfoState extends ChangeNotifier {
this._unreadMessages += contact["numUnread"] as int;
if (profileContact != null) {
profileContact.status = contact["status"];
profileContact.totalMessages = contact["numMessages"];
var newCount = contact["numMessages"];
if (newCount != profileContact.totalMessages) {
profileContact.messageCache.addFrontIndexGap(newCount - profileContact.totalMessages);
}
profileContact.totalMessages = newCount;
profileContact.unreadMessages = contact["numUnread"];
profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]));
} else {
@ -339,4 +345,13 @@ class ProfileInfoState extends ChangeNotifier {
_downloadTriggers[fileKey] = identifier;
notifyListeners();
}
int cacheMemUsage() {
return _contacts.cacheMemUsage();
}
void downloadReset(String fileKey) {
this._downloads.remove(fileKey);
notifyListeners();
}
}

View File

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'profile.dart';
@ -30,4 +31,8 @@ class ProfileListState extends ChangeNotifier {
}
int generateUnreadCount(String selectedProfile) => _profiles.where((p) => p.onion != selectedProfile).fold(0, (i, p) => i + p.unreadMessages);
int cacheMemUsage() {
return _profiles.map((e) => e.cacheMemUsage()).fold(0, (previousValue, element) => previousValue + element);
}
}

View File

@ -3,8 +3,10 @@ import 'dart:convert';
import 'dart:io';
import 'package:cwtch/main.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:win_toast/win_toast.dart';
//import 'package:desktop_notifications/desktop_notifications.dart';
import 'package:desktop_notifications/desktop_notifications.dart' as linux_notifications;
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
import 'package:flutter_local_notifications_linux/src/model/hint.dart';
@ -53,6 +55,52 @@ class WindowsNotificationManager implements NotificationsManager {
}
}
// LinuxNotificationsManager uses the desktop_notifications package to implement
// the standard dbus-powered linux desktop notifications.
class LinuxNotificationsManager implements NotificationsManager {
int previous_id = 0;
late linux_notifications.NotificationsClient client;
late Future<void> Function(String, int) notificationSelectConvo;
late String assetsPath;
LinuxNotificationsManager(Future<void> Function(String, int) notificationSelectConvo) {
this.client = linux_notifications.NotificationsClient();
this.notificationSelectConvo = notificationSelectConvo;
scheduleMicrotask(() async {
assetsPath = await detectLinuxAssetsPath();
});
}
// Cwtch can install in non flutter supported ways on linux, this code detects where the assets are on Linux
Future<String> detectLinuxAssetsPath() async {
var devStat = FileStat.stat("assets");
var localStat = FileStat.stat("data/flutter_assets");
var homeStat = FileStat.stat((Platform.environment["HOME"] ?? "") + "/.local/share/cwtch/data/flutter_assets");
var rootStat = FileStat.stat("/usr/share/cwtch/data/flutter_assets");
if ((await devStat).type == FileSystemEntityType.directory) {
return Directory.current.path; //appPath;
} else if ((await localStat).type == FileSystemEntityType.directory) {
return path.join(Directory.current.path, "data/flutter_assets/");
} else if ((await homeStat).type == FileSystemEntityType.directory) {
return (Platform.environment["HOME"] ?? "") + "/.local/share/cwtch/data/flutter_assets/";
} else if ((await rootStat).type == FileSystemEntityType.directory) {
return "/usr/share/cwtch/data/flutter_assets/";
}
return "";
}
Future<void> notify(String message, String profile, int conversationId) async {
var iconPath = Uri.file(path.join(assetsPath, "assets/knott.png"));
client.notify(message, appName: "cwtch", appIcon: iconPath.toString(), replacesId: this.previous_id).then((linux_notifications.Notification value) async {
previous_id = value.id;
if ((await value.closeReason) == linux_notifications.NotificationClosedReason.dismissed) {
this.notificationSelectConvo(profile, conversationId);
}
});
}
}
class NotificationPayload {
late String profileOnion;
late int convoId;
@ -72,7 +120,8 @@ class NotificationPayload {
};
}
// FlutterLocalNotificationsPlugin based NotificationManager that handles Linux and MacOS
// FlutterLocalNotificationsPlugin based NotificationManager that handles MacOS
// Todo: work with author to allow settings of asset_path so we can use this for Linux and deprecate the LinuxNotificationManager
// Todo: it can also handle Android, do we want to migrate away from our manual solution?
class NixNotificationManager implements NotificationsManager {
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
@ -81,19 +130,19 @@ class NixNotificationManager implements NotificationsManager {
NixNotificationManager(Future<void> Function(String, int) notificationSelectConvo) {
this.notificationSelectConvo = notificationSelectConvo;
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(defaultPresentSound: false);
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification', defaultIcon: AssetsLinuxIcon('assets/knott.png'), defaultSuppressSound: true);
final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux);
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
alert: true,
badge: false,
sound: false,
);
scheduleMicrotask(() async {
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
alert: true,
badge: false,
sound: false,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification);
});
}
@ -117,7 +166,13 @@ class NixNotificationManager implements NotificationsManager {
}
NotificationsManager newDesktopNotificationsManager(Future<void> Function(String profileOnion, int convoId) notificationSelectConvo) {
if ((Platform.isLinux && !Platform.isAndroid) || Platform.isMacOS) {
if (Platform.isLinux && !Platform.isAndroid) {
try {
return LinuxNotificationsManager(notificationSelectConvo);
} catch (e) {
EnvironmentConfig.debugLog("Failed to create LinuxNotificationManager. Switching off notifications.");
}
} else if (Platform.isMacOS) {
try {
return NixNotificationManager(notificationSelectConvo);
} catch (e) {

View File

@ -109,8 +109,8 @@ class Settings extends ChangeNotifier {
// single pane vs dual pane preferences
_uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]);
_uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]);
_notificationPolicy = notificationPolicyFromString(settings["NotificationPolicy"]);
_notificationContent = notificationContentFromString(settings["NotificationContent"]);
// auto-download folder
@ -275,7 +275,7 @@ class Settings extends ChangeNotifier {
static NotificationPolicy notificationPolicyFromString(String? np) {
switch (np) {
case "NotificationPolicy.None":
case "NotificationPolicy.Mute":
return NotificationPolicy.Mute;
case "NotificationPolicy.OptIn":
return NotificationPolicy.OptIn;

View File

@ -18,6 +18,7 @@ OpaqueThemeType GetMidnightTheme(String mode) {
class MidnightDark extends CwtchDark {
static final Color background = Color(0xFF1B1B1B);
static final Color backgroundAlt = Color(0xFF494949);
static final Color header = Color(0xFF1B1B1B);
static final Color userBubble = Color(0xFF373737);
static final Color peerBubble = Color(0xFF494949);
@ -41,6 +42,7 @@ class MidnightDark extends CwtchDark {
get messageFromOtherTextColor => font; //whiteishPurple;
get textfieldBackgroundColor => peerBubble;
get textfieldBorderColor => userBubble;
get backgroundHilightElementColor => backgroundAlt;
}
class MidnightLight extends CwtchLight {

View File

@ -128,8 +128,8 @@ ThemeData mkThemeData(Settings opaque) {
primaryIconTheme: IconThemeData(
color: opaque.current().mainTextColor,
),
primaryColor: opaque.current().backgroundMainColor,
canvasColor: opaque.current().backgroundPaneColor,
primaryColor: opaque.current().mainTextColor,
canvasColor: opaque.current().backgroundMainColor,
backgroundColor: opaque.current().backgroundMainColor,
highlightColor: opaque.current().hilightElementColor,
iconTheme: IconThemeData(
@ -154,6 +154,7 @@ ThemeData mkThemeData(Settings opaque) {
actionsIconTheme: IconThemeData(
color: opaque.current().mainTextColor,
)),
//bottomNavigationBarTheme: BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed, backgroundColor: opaque.current().backgroundHilightElementColor), // Can't determine current use
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
@ -181,7 +182,10 @@ ThemeData mkThemeData(Settings opaque) {
),
),
scrollbarTheme: ScrollbarThemeData(isAlwaysShown: false, thumbColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor)),
tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor))),
tabBarTheme: TabBarTheme(
labelColor: opaque.current().mainTextColor,
unselectedLabelColor: opaque.current().mainTextColor,
indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor))),
dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor,
titleTextStyle: TextStyle(color: opaque.current().mainTextColor),
@ -207,8 +211,14 @@ ThemeData mkThemeData(Settings opaque) {
thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor),
trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor),
),
// the only way to change the text Selection Context Menu Color ?!
brightness: opaque.current().mode == mode_dark ? Brightness.dark : Brightness.light,
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: opaque.current().defaultButtonColor, hoverColor: opaque.current().defaultButtonActiveColor, enableFeedback: true, splashColor: opaque.current().defaultButtonActiveColor),
foregroundColor: opaque.current().mainTextColor,
backgroundColor: opaque.current().defaultButtonColor,
hoverColor: opaque.current().defaultButtonActiveColor,
enableFeedback: true,
splashColor: opaque.current().defaultButtonActiveColor),
textSelectionTheme: TextSelectionThemeData(
cursorColor: opaque.current().defaultButtonActiveColor, selectionColor: opaque.current().defaultButtonActiveColor, selectionHandleColor: opaque.current().defaultButtonActiveColor),
);

View File

@ -171,6 +171,9 @@ class SelectableLinkify extends StatelessWidget {
// TextSpan
/// Style for code text
final TextStyle? codeStyle;
/// Style for non-link text
final TextStyle? style;
@ -255,6 +258,7 @@ class SelectableLinkify extends StatelessWidget {
this.linkStyle,
// RichText
this.textAlign,
this.codeStyle,
this.textDirection,
this.minLines,
this.maxLines,
@ -291,6 +295,7 @@ class SelectableLinkify extends StatelessWidget {
buildTextSpan(
elements,
style: Theme.of(context).textTheme.bodyText2?.merge(style),
codeStyle: Theme.of(context).textTheme.bodyText2?.merge(codeStyle),
onOpen: onOpen,
linkStyle: Theme.of(context)
.textTheme
@ -347,6 +352,7 @@ TextSpan buildTextSpan(
List<LinkifyElement> elements, {
TextStyle? style,
TextStyle? linkStyle,
TextStyle? codeStyle,
LinkCallback? onOpen,
bool useMouseRegion = false,
}) {
@ -404,7 +410,7 @@ TextSpan buildTextSpan(
text: element.text.replaceAll("\`", ""),
// monospace fonts at the same size as regular text makes them appear
// slightly larger, so we compensate by making them slightly smaller...
style: style?.copyWith(fontFamily: "RobotoMono", fontSize: style.fontSize! - 1.0),
style: codeStyle?.copyWith(fontFamily: "RobotoMono", fontSize: codeStyle.fontSize! - 1.5),
semanticsLabel: element.text);
} else {
return TextSpan(

View File

@ -21,6 +21,10 @@ import '../main.dart';
/// NOTE: This view makes use of the global Error Handler to receive events from the Cwtch Library (for validating
/// error states caused by incorrect import string or duplicate requests to add a specific contact)
class AddContactView extends StatefulWidget {
final newGroup;
const AddContactView({Key? key, this.newGroup}) : super(key: key);
@override
_AddContactViewState createState() => _AddContactViewState();
}
@ -52,9 +56,10 @@ class _AddContactViewState extends State<AddContactView> {
ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
/// We display a different number of tabs depending on the experiment setup
bool groupsEnabled = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
return Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) {
bool groupsEnabled = Provider.of<Settings>(context, listen: false).isExperimentEnabled(TapirGroupsExperiment);
return Consumer<ErrorHandler>(builder: (bcontext, globalErrorHandler, child) {
return DefaultTabController(
initialIndex: widget.newGroup && groupsEnabled ? 1 : 0,
length: groupsEnabled ? 2 : 1,
child: Column(children: [
(groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()),
@ -62,10 +67,10 @@ class _AddContactViewState extends State<AddContactView> {
child: TabBarView(
children: (groupsEnabled
? [
addPeerTab(),
addGroupTab(),
addPeerTab(bcontext),
addGroupTab(bcontext),
]
: [addPeerTab()]),
: [addPeerTab(bcontext)]),
)),
]));
});
@ -105,7 +110,7 @@ class _AddContactViewState extends State<AddContactView> {
/// The Add Peer Tab allows a peer to add a specific non-group peer to their contact lists
/// We also provide a convenient way to copy their onion.
Widget addPeerTab() {
Widget addPeerTab(bcontext) {
return Scrollbar(
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
@ -152,18 +157,18 @@ class _AddContactViewState extends State<AddContactView> {
return null;
},
onChanged: (String importBundle) async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle.replaceFirst("cwtch:", ""));
var profileOnion = Provider.of<ProfileInfoState>(bcontext, listen: false).onion;
Provider.of<FlwtchState>(bcontext, listen: false).cwtch.ImportBundle(profileOnion, importBundle.replaceFirst("cwtch:", ""));
Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.importBundleSuccess) {
// TODO: This isn't ideal, but because onChange can be fired during this future check
// and because the context can change after being popped we have this kind of double assertion...
// There is probably a better pattern to handle this...
if (AppLocalizations.of(context) != null) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + importBundle));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.popUntil(context, (route) => route.settings.name == "conversations");
if (AppLocalizations.of(bcontext) != null) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(bcontext)!.successfullAddedContact + importBundle));
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
Navigator.popUntil(bcontext, (route) => route.settings.name == "conversations");
}
}
});
@ -174,10 +179,10 @@ class _AddContactViewState extends State<AddContactView> {
}
/// TODO Add Group Pane
Widget addGroupTab() {
Widget addGroupTab(bcontext) {
// TODO We should replace with with a "Paste in Server Key Bundle"
if (Provider.of<ProfileInfoState>(context).serverList.servers.isEmpty) {
return Text(AppLocalizations.of(context)!.addServerFirst);
if (Provider.of<ProfileInfoState>(bcontext).serverList.servers.isEmpty) {
return Text(AppLocalizations.of(bcontext)!.addServerFirst);
}
return Scrollbar(
@ -205,11 +210,7 @@ class _AddContactViewState extends State<AddContactView> {
},
isExpanded: true, // magic property
value: server,
items: Provider.of<ProfileInfoState>(context)
.serverList
.servers
.where((serverInfo) => serverInfo.status == "Synced")
.map<DropdownMenuItem<String>>((RemoteServerInfoState serverInfo) {
items: Provider.of<ProfileInfoState>(bcontext).serverList.servers.map<DropdownMenuItem<String>>((RemoteServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(
@ -221,13 +222,13 @@ class _AddContactViewState extends State<AddContactView> {
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.groupNameLabel),
CwtchLabel(label: AppLocalizations.of(bcontext)!.groupNameLabel),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrGroupName,
hintText: AppLocalizations.of(context)!.groupNameLabel,
hintText: AppLocalizations.of(bcontext)!.groupNameLabel,
onChanged: (newValue) {},
validator: (value) {},
),
@ -236,12 +237,12 @@ class _AddContactViewState extends State<AddContactView> {
),
ElevatedButton(
onPressed: () {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateGroup(profileOnion, server, ctrlrGroupName.text);
var profileOnion = Provider.of<ProfileInfoState>(bcontext, listen: false).onion;
Provider.of<FlwtchState>(bcontext, listen: false).cwtch.CreateGroup(profileOnion, server, ctrlrGroupName.text);
Future.delayed(const Duration(milliseconds: 500), () {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + " " + ctrlrGroupName.text));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
Navigator.pop(bcontext);
});
},
child: Text(AppLocalizations.of(context)!.createGroupBtn),
@ -249,20 +250,4 @@ class _AddContactViewState extends State<AddContactView> {
],
)))));
}
/// TODO Manage Servers Tab
Widget manageServersTab() {
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((RemoteServerInfoState server) {
return ChangeNotifierProvider<RemoteServerInfoState>.value(
value: server,
child: ListTile(
title: Text(server.onion),
));
});
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return ListView(children: divided);
}
}

View File

@ -87,7 +87,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
child: Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [
Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
@ -127,6 +127,9 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
badgeEdit: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment))))
])),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
SizedBox(
height: 20,
@ -273,64 +276,71 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: _createPressed,
child: Text(
Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn,
textAlign: TextAlign.center,
),
),
),
],
ElevatedButton(
onPressed: _createPressed,
style: ElevatedButton.styleFrom(
minimumSize: Size(400, 50),
maximumSize: Size(800, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn,
textAlign: TextAlign.center,
),
),
SizedBox(
height: 20,
),
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.exportProfileTooltip,
child: ElevatedButton.icon(
onPressed: () {
if (Platform.isAndroid) {
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, ctrlrOnion.value.text + ".tar.gz");
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + ctrlrOnion.value.text + ".tar.gz"));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
showCreateFilePicker(context).then((name) {
if (name != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
});
}
},
icon: Icon(Icons.import_export),
label: Text(AppLocalizations.of(context)!.exportProfile),
))
])),
child: Tooltip(
message: AppLocalizations.of(context)!.exportProfileTooltip,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
minimumSize: Size(400, 50),
maximumSize: Size(800, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
onPressed: () {
if (Platform.isAndroid) {
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, ctrlrOnion.value.text + ".tar.gz");
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + ctrlrOnion.value.text + ".tar.gz"));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
showCreateFilePicker(context).then((name) {
if (name != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
});
}
},
icon: Icon(Icons.import_export),
label: Text(AppLocalizations.of(context)!.exportProfile),
))),
SizedBox(
height: 20,
),
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
))
]))
child: Tooltip(
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
minimumSize: Size(400, 50),
maximumSize: Size(800, 50),
shape: RoundedRectangleBorder(
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
primary: Provider.of<Settings>(context).theme.backgroundMainColor,
),
onPressed: () {
showAlertDialog(context);
},
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
)))
]))))));
});
});

View File

@ -29,16 +29,21 @@ class ContactsView extends StatefulWidget {
// selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, int handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
var unread = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
if (previouslySelected != null) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
}
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected();
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).initialScrollIndex = initialIndex;
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread;
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
// Set last message seen time in backend
Provider.of<FlwtchState>(context, listen: false)
.cwtch
@ -128,9 +133,12 @@ class _ContactsViewState extends State<ContactsView> {
actions: getActions(context),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddContact,
onPressed: _modalAddImportChoice,
tooltip: AppLocalizations.of(context)!.tooltipAddContact,
child: const Icon(CwtchIcons.person_add_alt_1_24px),
child: Icon(
CwtchIcons.person_add_alt_1_24px,
color: Provider.of<Settings>(context).theme.defaultButtonTextColor,
),
),
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList());
}
@ -204,14 +212,18 @@ class _ContactsViewState extends State<ContactsView> {
return RepaintBoundary(child: ListView(children: divided));
}
void _pushAddContact() {
void _pushAddContact(bool newGroup) {
// close modal
Navigator.popUntil(context, (route) => route.settings.name == "conversations");
// open add contact / create group pane
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext bcontext) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
],
child: AddContactView(),
child: AddContactView(newGroup: newGroup),
);
},
));
@ -228,4 +240,104 @@ class _ContactsViewState extends State<ContactsView> {
},
));
}
void _modalAddImportChoice() {
bool groupsEnabled = Provider.of<Settings>(context, listen: false).isExperimentEnabled(TapirGroupsExperiment);
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return Padding(
padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary(
child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(2.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: AppLocalizations.of(context)!.tooltipAddContact,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addContact,
semanticsLabel: AppLocalizations.of(context)!.addContact,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
_pushAddContact(false);
},
))),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addServerTitle,
semanticsLabel: AppLocalizations.of(context)!.addServerTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled
? () {
_pushAddContact(false);
}
: null,
)),
),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.createGroupTitle,
semanticsLabel: AppLocalizations.of(context)!.createGroupTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled
? () {
_pushAddContact(true);
}
: null,
))),
SizedBox(
height: 20,
),
],
))),
)));
});
}
}

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/folderpicker.dart';
@ -13,6 +14,7 @@ import 'package:cwtch/themes/opaque.dart';
import 'package:cwtch/themes/pumpkin.dart';
import 'package:cwtch/themes/vampire.dart';
import 'package:cwtch/themes/witch.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
@ -29,11 +31,53 @@ class GlobalSettingsView extends StatefulWidget {
}
class _GlobalSettingsViewState extends State<GlobalSettingsView> {
static const androidSettingsChannel = const MethodChannel('androidSettings');
static const androidSettingsChangeChannel = const MethodChannel('androidSettingsChanged');
bool powerExempt = false;
@override
void dispose() {
super.dispose();
}
@override
void initState() {
super.initState();
androidSettingsChangeChannel.setMethodCallHandler(handleSettingsChanged);
if (Platform.isAndroid) {
isBatteryExempt().then((value) => setState(() {
powerExempt = value;
}));
} else {
powerExempt = false;
}
}
// Handler on method channel for MainActivity/onActivityResult to report the user choice when we ask for power exemption
Future<void> handleSettingsChanged(MethodCall call) async {
if (call.method == "powerExemptionChange") {
if (call.arguments) {
setState(() {
powerExempt = true;
});
}
}
}
//* Android Only Requests
Future<bool> isBatteryExempt() async {
return await androidSettingsChannel.invokeMethod('isBatteryExempt', {}) ?? false;
}
Future<void> requestBatteryExemption() async {
await androidSettingsChannel.invokeMethod('requestBatteryExemption', {});
return Future.value();
}
//* End Android Only Requests
@override
Widget build(BuildContext context) {
return Scaffold(
@ -63,20 +107,23 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
ListTile(
title: Text(AppLocalizations.of(context)!.settingLanguage, style: TextStyle(color: settings.current().mainTextColor)),
leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor),
trailing: DropdownButton(
value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String? newValue) {
setState(() {
settings.switchLocale(Locale(newValue!));
saveSettings(context);
});
},
items: AppLocalizations.supportedLocales.map<DropdownMenuItem<String>>((Locale value) {
return DropdownMenuItem<String>(
value: value.languageCode,
child: Text(getLanguageFull(context, value.languageCode)),
);
}).toList())),
trailing: Container(
width: MediaQuery.of(context).size.width / 4,
child: DropdownButton(
isExpanded: true,
value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String? newValue) {
setState(() {
settings.switchLocale(Locale(newValue!));
saveSettings(context);
});
},
items: AppLocalizations.supportedLocales.map<DropdownMenuItem<String>>((Locale value) {
return DropdownMenuItem<String>(
value: value.languageCode,
child: Text(getLanguageFull(context, value.languageCode)),
);
}).toList()))),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingTheme, style: TextStyle(color: settings.current().mainTextColor)),
value: settings.current().mode == mode_light,
@ -96,39 +143,44 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
),
ListTile(
title: Text(AppLocalizations.of(context)!.themeColorLabel),
trailing: DropdownButton<String>(
key: Key("DropdownTheme"),
isDense: true,
value: Provider.of<Settings>(context).theme.theme,
onChanged: (String? newValue) {
setState(() {
settings.setTheme(newValue!, settings.theme.mode);
saveSettings(context);
});
},
items: themes.keys.map<DropdownMenuItem<String>>((String themeId) {
return DropdownMenuItem<String>(
value: themeId,
child: Text(getThemeName(context, themeId)), //"ddi_$themeId", key: Key("ddi_$themeId")),
);
}).toList()),
leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
trailing: Container(
width: MediaQuery.of(context).size.width / 4,
child: DropdownButton<String>(
key: Key("DropdownTheme"),
isExpanded: true,
value: Provider.of<Settings>(context).theme.theme,
onChanged: (String? newValue) {
setState(() {
settings.setTheme(newValue!, settings.theme.mode);
saveSettings(context);
});
},
items: themes.keys.map<DropdownMenuItem<String>>((String themeId) {
return DropdownMenuItem<String>(
value: themeId,
child: Text(getThemeName(context, themeId)), //"ddi_$themeId", key: Key("ddi_$themeId")),
);
}).toList())),
leading: Icon(Icons.palette, color: settings.current().mainTextColor),
),
ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor)),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
trailing: DropdownButton(
value: settings.uiColumnModePortrait.toString(),
onChanged: (String? newValue) {
settings.uiColumnModePortrait = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(false).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value, context)),
);
}).toList())),
trailing: Container(
width: MediaQuery.of(context).size.width / 4,
child: DropdownButton(
isExpanded: true,
value: settings.uiColumnModePortrait.toString(),
onChanged: (String? newValue) {
settings.uiColumnModePortrait = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(false).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value, context)),
);
}).toList()))),
ListTile(
title: Text(
AppLocalizations.of(context)!.settingUIColumnLandscape,
@ -136,25 +188,27 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
softWrap: true,
style: TextStyle(color: settings.current().mainTextColor),
),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
leading: Icon(Icons.stay_primary_landscape, color: settings.current().mainTextColor),
trailing: Container(
width: MediaQuery.of(context).size.width / 4,
child: DropdownButton(
isExpanded: true,
value: settings.uiColumnModeLandscape.toString(),
onChanged: (String? newValue) {
settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(
Settings.uiColumnModeToString(value, context),
overflow: TextOverflow.ellipsis,
),
);
}).toList()))),
child: Container(
width: MediaQuery.of(context).size.width / 4,
child: DropdownButton(
isExpanded: true,
value: settings.uiColumnModeLandscape.toString(),
onChanged: (String? newValue) {
settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(
Settings.uiColumnModeToString(value, context),
overflow: TextOverflow.ellipsis,
),
);
}).toList())))),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.streamerModeLabel, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.descriptionStreamerMode),
@ -172,24 +226,46 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
height: 40,
),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [Text(AppLocalizations.of(context)!.settingGroupBehaviour, style: TextStyle(fontWeight: FontWeight.bold))]),
Visibility(
visible: Platform.isAndroid,
child: SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingAndroidPowerExemption, style: TextStyle(color: settings.current().mainTextColor)),
subtitle: Text(AppLocalizations.of(context)!.settingAndroidPowerExemptionDescription),
value: powerExempt,
onChanged: (bool value) {
if (value) {
requestBatteryExemption();
} else {
// We can't ask for it to be turned off, show an informational popup
showBatteryDialog(context);
}
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.power, color: settings.current().mainTextColor),
),
),
ListTile(
title: Text(AppLocalizations.of(context)!.notificationPolicySettingLabel),
subtitle: Text(AppLocalizations.of(context)!.notificationPolicySettingDescription),
trailing: DropdownButton(
value: settings.notificationPolicy,
onChanged: (NotificationPolicy? newValue) {
settings.notificationPolicy = newValue!;
saveSettings(context);
},
items: NotificationPolicy.values.map<DropdownMenuItem<NotificationPolicy>>((NotificationPolicy value) {
return DropdownMenuItem<NotificationPolicy>(
value: value,
child: Text(
Settings.notificationPolicyToString(value, context),
overflow: TextOverflow.ellipsis,
),
);
}).toList()),
trailing: Container(
width: MediaQuery.of(context).size.width / 4,
child: DropdownButton(
isExpanded: true,
value: settings.notificationPolicy,
onChanged: (NotificationPolicy? newValue) {
settings.notificationPolicy = newValue!;
saveSettings(context);
},
items: NotificationPolicy.values.map<DropdownMenuItem<NotificationPolicy>>((NotificationPolicy value) {
return DropdownMenuItem<NotificationPolicy>(
value: value,
child: Text(
Settings.notificationPolicyToString(value, context),
overflow: TextOverflow.ellipsis,
),
);
}).toList())),
leading: Icon(CwtchIcons.chat_bubble_empty_24px, color: settings.current().mainTextColor),
),
ListTile(
@ -330,7 +406,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
},
activeTrackColor: settings.theme.defaultButtonActiveColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor),
secondary: Icon(Icons.photo, color: settings.current().mainTextColor),
),
Visibility(
visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid,
@ -398,10 +474,53 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
child: SelectableText(AppLocalizations.of(context)!.versionBuilddate.replaceAll("%1", EnvironmentConfig.BUILD_VER).replaceAll("%2", EnvironmentConfig.BUILD_DATE)),
)
]),
Visibility(
visible: EnvironmentConfig.BUILD_VER == dev_version && !Platform.isAndroid,
child: FutureBuilder(
future: EnvironmentConfig.BUILD_VER != dev_version || Platform.isAndroid ? null : Provider.of<FlwtchState>(context).cwtch.GetDebugInfo(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: [
Text("libCwtch Debug Info: " + snapshot.data.toString()),
Text("Message Cache Size (Mb): " + (Provider.of<FlwtchState>(context).profs.cacheMemUsage() / (1024 * 1024)).toString())
],
);
} else {
return Container();
}
},
),
)
]))));
});
});
}
showBatteryDialog(BuildContext context) {
Widget okButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.okButton),
onPressed: () {
Navigator.of(context).pop();
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.settingsAndroidPowerReenablePopup),
actions: [
okButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}
/// Construct a version string from Package Info

View File

@ -183,7 +183,9 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
onPressed: () {
showAlertDialog(context);
},
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.backgroundPaneColor),
foregroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.mainTextColor)),
icon: Icon(CwtchIcons.leave_group),
label: Text(
AppLocalizations.of(context)!.leaveConversation,

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:crypto/crypto.dart';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
@ -7,8 +8,10 @@ import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/chatmessage.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messagecache.dart';
import 'package:cwtch/models/messages/quotedmessage.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/profileimage.dart';
@ -24,6 +27,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../config.dart';
import '../constants.dart';
import '../main.dart';
import '../settings.dart';
@ -40,8 +44,9 @@ class _MessageViewState extends State<MessageView> {
final focusNode = FocusNode();
int selectedContact = -1;
ItemPositionsListener scrollListener = ItemPositionsListener.create();
ItemScrollController scrollController = ItemScrollController();
File? imagePreview;
bool showDown = false;
bool showPreview = false;
@override
void initState() {
@ -52,6 +57,12 @@ class _MessageViewState extends State<MessageView> {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
}
if (scrollListener.itemPositions.value.length != 0 && !scrollListener.itemPositions.value.any((element) => element.index == 0)) {
showDown = true;
} else {
showDown = false;
}
});
super.initState();
}
@ -79,10 +90,11 @@ class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) {
// After leaving a conversation the selected conversation is set to null...
if (Provider.of<ContactInfoState>(context).profileOnion == "") {
if (Provider.of<ContactInfoState>(context, listen: false).profileOnion == "") {
return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)));
}
var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
var appBarButtons = <Widget>[];
if (Provider.of<ContactInfoState>(context).isOnline()) {
@ -126,18 +138,26 @@ class _MessageViewState extends State<MessageView> {
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor,
floatingActionButton: appState.unreadMessagesBelow
floatingActionButton: showDown
? FloatingActionButton(
child: Icon(Icons.arrow_downward),
child: Icon(Icons.arrow_downward, color: Provider.of<Settings>(context).current().defaultButtonTextColor),
onPressed: () {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
Provider.of<ContactInfoState>(context).messageScrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
})
: null,
appBar: AppBar(
// setting leading to null makes it do the default behaviour; container() hides it
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
// setting leading(Width) to null makes it do the default behaviour; container() hides it
leadingWidth: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? 0 : null,
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1
? Container(
padding: EdgeInsets.zero,
margin: EdgeInsets.zero,
width: 0,
height: 0,
)
: null,
title: Row(children: [
ProfileImage(
imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
@ -159,13 +179,23 @@ class _MessageViewState extends State<MessageView> {
]),
actions: appBarButtons,
),
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)),
bottomSheet: _buildComposeBox(),
body: Padding(
padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 182.0),
child: MessageList(
scrollListener,
)),
bottomSheet: showPreview && showMessageFormattingPreview ? _buildPreviewBox() : _buildComposeBox(),
));
}
Future<bool> _onWillPop() async {
Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0;
var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
if (previouslySelected != null) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
}
Provider.of<AppState>(context, listen: false).selectedConversation = null;
return true;
}
@ -216,14 +246,13 @@ class _MessageViewState extends State<MessageView> {
if (ctrlrCompose.value.text.isNotEmpty && lengthOk) {
if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<AppState>(context, listen: false).selectedIndex != null) {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.GetMessageByID(Provider.of<AppState>(context, listen: false).selectedProfile!, Provider.of<AppState>(context, listen: false).selectedConversation!,
Provider.of<AppState>(context, listen: false).selectedIndex!)
.then((data) {
var conversationId = Provider.of<AppState>(context, listen: false).selectedConversation!;
MessageCache? cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationId)?.messageCache;
ById(Provider.of<AppState>(context, listen: false).selectedIndex!)
.get(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(context, listen: false).selectedProfile!, conversationId, cache!)
.then((MessageInfo? data) {
try {
var messageWrapper = jsonDecode(data! as String);
var bytes1 = utf8.encode(messageWrapper["PeerID"] + messageWrapper['Message']);
var bytes1 = utf8.encode(data!.metadata.senderHandle + data.wrapper);
var digest1 = sha256.convert(bytes1);
var contentHash = base64Encode(digest1.bytes);
var quotedMessage = jsonEncode(QuotedMessageStructure(contentHash, ctrlrCompose.value.text));
@ -232,7 +261,9 @@ class _MessageViewState extends State<MessageView> {
.cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm))
.then(_sendMessageHandler);
} catch (e) {}
} catch (e) {
EnvironmentConfig.debugLog("Exception: reply to message could not be found: " + e.toString());
}
Provider.of<AppState>(context, listen: false).selectedIndex = null;
});
} else {
@ -286,64 +317,216 @@ class _MessageViewState extends State<MessageView> {
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, identifier, LastMessageSeenTimeKey, DateTime.now().toIso8601String());
}
Widget _buildComposeBox() {
bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
Widget _buildPreviewBox() {
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
var charLength = ctrlrCompose.value.text.characters.length;
var expectedLength = ctrlrCompose.value.text.length;
var numberOfBytesMoreThanChar = (expectedLength - charLength);
var wdgMessage = Padding(
padding: EdgeInsets.all(8),
child: SelectableLinkify(
text: ctrlrCompose.text + '\n',
options: LinkifyOptions(messageFormatting: true, parseLinks: showClickableLinks, looseUrl: true, defaultToHttps: true),
linkifiers: [UrlLinkifier()],
onOpen: showClickableLinks ? null : null,
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
fontSize: 16,
),
linkStyle: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
fontSize: 16,
),
codeStyle: TextStyle(
// note: these colors are flipped
fontSize: 16,
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
backgroundColor: Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
));
var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var preview = showMessageFormattingPreview
? IconButton(
icon: Icon(Icons.text_fields),
onPressed: () {
setState(() {
showPreview = false;
});
})
: Container();
var composeBox = Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor,
padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2),
height: 100,
child: Row(
children: <Widget>[
Expanded(
child: Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))),
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: handleKeyPress,
child: Padding(
padding: EdgeInsets.all(8),
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
focusNode: focusNode,
autofocus: !Platform.isAndroid,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
enableIMEPersonalizedLearning: false,
minLines: 1,
maxLength: (isGroup ? GroupMessageLengthMax : P2PMessageLengthMax) - numberOfBytesMoreThanChar,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
maxLines: null,
onFieldSubmitted: _sendMessage,
enabled: true, // always allow editing...
onChanged: (String x) {
setState(() {
// we need to force a rerender here to update the max length count
});
},
decoration: InputDecoration(
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.sendHintTextColor),
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
enabled: true,
suffixIcon: ElevatedButton(
key: Key("btnSend"),
style: ElevatedButton.styleFrom(padding: EdgeInsets.all(0.0), shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(45.0))),
child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
onPressed: isOffline ? null : _sendMessage,
))),
)))),
// 164 minimum height + 16px for every line of text so the entire message is displayed when previewed.
height: 164 + ((ctrlrCompose.text.split("\n").length - 1) * 16),
child: Column(
children: [
Row(mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [preview]),
Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))),
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [wdgMessage])),
],
),
);
return Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [composeBox]));
}
Widget _buildComposeBox() {
bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
var showToolbar = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var charLength = ctrlrCompose.value.text.characters.length;
var expectedLength = ctrlrCompose.value.text.length;
var numberOfBytesMoreThanChar = (expectedLength - charLength);
var bold = IconButton(
icon: Icon(Icons.format_bold),
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "**" + selected + "**");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
});
});
var italic = IconButton(
icon: Icon(Icons.format_italic),
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "*" + selected + "*");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var code = IconButton(
icon: Icon(Icons.code),
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "`" + selected + "`");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var superscript = IconButton(
icon: Icon(Icons.superscript),
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "^" + selected + "^");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var subscript = IconButton(
icon: Icon(Icons.subscript),
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "_" + selected + "_");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var strikethrough = IconButton(
icon: Icon(Icons.format_strikethrough),
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "~~" + selected + "~~");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
});
});
var preview = IconButton(
icon: Icon(Icons.text_format),
onPressed: () {
setState(() {
showPreview = true;
});
});
var vline = Padding(
padding: EdgeInsets.symmetric(vertical: 1, horizontal: 2),
child: Container(height: 16, width: 1, decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)));
var formattingToolbar = Container(
decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor),
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [bold, italic, code, superscript, subscript, strikethrough, vline, preview]));
var textField = Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))),
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: handleKeyPress,
child: Padding(
padding: EdgeInsets.all(8),
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
focusNode: focusNode,
autofocus: !Platform.isAndroid,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
enableIMEPersonalizedLearning: false,
minLines: 1,
maxLength: (isGroup ? GroupMessageLengthMax : P2PMessageLengthMax) - numberOfBytesMoreThanChar,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
maxLines: 3,
onFieldSubmitted: _sendMessage,
enabled: true, // always allow editing...
onChanged: (String x) {
setState(() {
// we need to force a rerender here to update the max length count
});
},
decoration: InputDecoration(
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.sendHintTextColor),
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
enabled: true,
suffixIcon: ElevatedButton(
key: Key("btnSend"),
style: ElevatedButton.styleFrom(padding: EdgeInsets.all(0.0), shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(45.0))),
child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
onPressed: isOffline ? null : _sendMessage,
))),
)));
var textEditChildren;
if (showToolbar) {
textEditChildren = [formattingToolbar, textField];
} else {
textEditChildren = [textField];
}
var composeBox =
Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, padding: EdgeInsets.all(2), margin: EdgeInsets.all(2), height: 164, child: Column(children: textEditChildren));
var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
@ -392,7 +575,7 @@ class _MessageViewState extends State<MessageView> {
children = [composeBox];
}
return Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, children: children));
return Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: children));
}
// Send the message if enter is pressed without the shift key...

View File

@ -70,7 +70,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
textAlign: TextAlign.left,
text: TextSpan(
text: country,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 10, fontFamily: "monospace"),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 10, fontFamily: "RobotoMono"),
children: [TextSpan(text: " ($ip)", style: TextStyle(fontSize: 8, fontWeight: FontWeight.normal))]));
}).toList(growable: true);
@ -140,7 +140,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
CwtchButtonTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion),
onPressed: _copyOnion,
icon: Icon(Icons.copy),
icon: Icon(CwtchIcons.address_copy_2),
tooltip: AppLocalizations.of(context)!.copyBtn,
)
]),
@ -269,7 +269,9 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
onPressed: () {
showAlertDialog(context);
},
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.backgroundPaneColor),
foregroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.mainTextColor)),
icon: Icon(CwtchIcons.leave_group),
label: Text(
AppLocalizations.of(context)!.leaveConversation,

View File

@ -75,6 +75,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
color: Provider.of<Settings>(context).theme.defaultButtonTextColor,
),
),
body: _buildProfileManager(),
@ -193,49 +194,66 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.addProfileTitle, semanticsLabel: AppLocalizations.of(context)!.addProfileTitle),
onPressed: () {
_pushAddProfile(context);
},
)),
Spacer()
]),
SizedBox(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip(
message: AppLocalizations.of(context)!.importProfileTooltip,
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile),
onPressed: () {
// 10GB profiles should be enough for anyone?
showFilePicker(context, MaxGeneralFileSharingSize, (file) {
showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
Navigator.popUntil(context, (route) => route.isFirst);
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
if (value == "") {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
});
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 20),
maximumSize: Size(400, 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addProfileTitle,
semanticsLabel: AppLocalizations.of(context)!.addProfileTitle,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
_pushAddProfile(context);
},
)),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: AppLocalizations.of(context)!.importProfileTooltip,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 20),
maximumSize: Size(400, 20),
shape: RoundedRectangleBorder(
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
primary: Provider.of<Settings>(context).theme.backgroundMainColor,
),
child:
Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile, style: TextStyle(fontWeight: FontWeight.bold)),
onPressed: () {
// 10GB profiles should be enough for anyone?
showFilePicker(context, MaxGeneralFileSharingSize, (file) {
showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
Navigator.popUntil(context, (route) => route.isFirst);
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
if (value == "") {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
});
}, () {}, () {});
},
))),
Spacer()
]),
});
}, () {}, () {});
},
))),
SizedBox(
height: 20,
),
],
))),
)));

View File

@ -41,6 +41,7 @@ class _ServersView extends State<ServersView> {
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
color: Provider.of<Settings>(context).theme.defaultButtonTextColor,
),
),
body: Consumer<ServerListState>(

View File

@ -3,11 +3,8 @@ import 'dart:io';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:cwtch/views/messageview.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -40,7 +37,7 @@ class _ContactRowState extends State<ContactRow> {
return Card(
clipBehavior: Clip.antiAlias,
color: Provider.of<AppState>(context).selectedConversation == contact.identifier ? Provider.of<Settings>(context).theme.backgroundHilightElementColor : null,
borderOnForeground: false,
borderOnForeground: true,
margin: EdgeInsets.all(0.0),
child: InkWell(
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
@ -80,44 +77,52 @@ class _ContactRowState extends State<ContactRow> {
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: Text(contact.onion,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor : Provider.of<Settings>(context).theme.mainTextColor)),
)
),
Container(
padding: EdgeInsets.all(0),
child: contact.isInvitation == true
? Wrap(alignment: WrapAlignment.start, direction: Axis.vertical, children: <Widget>[
Padding(
padding: EdgeInsets.all(2),
child: TextButton.icon(
label: Text(
AppLocalizations.of(context)!.tooltipAcceptContactRequest,
),
icon: Icon(
Icons.favorite,
size: 16,
color: Provider.of<Settings>(context).theme.mainTextColor,
),
onPressed: _btnApprove,
)),
Padding(
padding: EdgeInsets.all(2),
child: TextButton.icon(
label: Text(
AppLocalizations.of(context)!.tooltipRejectContactRequest,
style: TextStyle(decoration: TextDecoration.underline),
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.backgroundPaneColor),
foregroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.mainTextColor)),
icon: Icon(Icons.delete, size: 16, color: Provider.of<Settings>(context).theme.mainTextColor),
onPressed: _btnReject,
))
])
: (contact.isBlocked != null && contact.isBlocked
? IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(Icons.block, color: Provider.of<Settings>(context).theme.mainTextColor),
onPressed: () {},
)
: Text(dateToNiceString(contact.lastMessageTime))),
),
],
))),
Padding(
padding: const EdgeInsets.all(5.0),
child: contact.isInvitation == true
? Wrap(direction: Axis.vertical, children: <Widget>[
IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(
Icons.favorite,
color: Provider.of<Settings>(context).theme.mainTextColor,
),
tooltip: AppLocalizations.of(context)!.tooltipAcceptContactRequest,
onPressed: _btnApprove,
),
IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(Icons.delete, color: Provider.of<Settings>(context).theme.mainTextColor),
tooltip: AppLocalizations.of(context)!.tooltipRejectContactRequest,
onPressed: _btnReject,
)
])
: (contact.isBlocked != null && contact.isBlocked
? IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(Icons.block, color: Provider.of<Settings>(context).theme.mainTextColor),
onPressed: () {},
)
: Text(dateToNiceString(contact.lastMessageTime))),
),
]),
onTap: () {
selectConversation(context, contact.identifier);
@ -148,7 +153,7 @@ class _ContactRowState extends State<ContactRow> {
return AppLocalizations.of(context)!.conversationNotificationPolicyNever;
}
// If the last message was over a day ago, just state the date
if (DateTime.now().difference(date).inDays > 1) {
if (DateTime.now().difference(date).inDays > 0) {
return DateFormat.yMd(Platform.localeName).format(date.toLocal());
}
// Otherwise just state the time.

View File

@ -25,8 +25,9 @@ class FileBubble extends StatefulWidget {
final int fileSize;
final bool interactive;
final bool isAuto;
final bool isPreview;
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true});
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true, this.isPreview = false});
@override
FileBubbleState createState() => FileBubbleState();
@ -44,11 +45,27 @@ class FileBubbleState extends State<FileBubble> {
super.initState();
}
Widget getPreview(context) {
return Image.file(
myFile!,
cacheWidth: (MediaQuery.of(context).size.width * 0.6).floor(),
// limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.30,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
);
}
@override
Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var flagStarted = Provider.of<MessageMetadata>(context).attributes["file-downloaded"] == "true";
var borderRadiousEh = 15.0;
var borderRadius = 15.0;
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
@ -56,7 +73,7 @@ class FileBubbleState extends State<FileBubble> {
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey());
// If we haven't stored the filepath in message attributes then save it
if (metadata.attributes["filepath"] != null) {
if (metadata.attributes["filepath"] != null && metadata.attributes["filepath"].toString().isNotEmpty) {
path = metadata.attributes["filepath"];
} else if (path != null && metadata.attributes["filepath"] == null) {
Provider.of<FlwtchState>(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", path);
@ -72,6 +89,21 @@ class FileBubbleState extends State<FileBubble> {
if (myFile == null) {
setState(() {
myFile = new File(path!);
// reset
if (myFile?.existsSync() == false) {
myFile = null;
Provider.of<ProfileInfoState>(context).downloadReset(widget.fileKey());
Provider.of<MessageMetadata>(context).attributes["filepath"] = null;
Provider.of<MessageMetadata>(context).attributes["file-downloaded"] = "false";
Provider.of<MessageMetadata>(context).attributes["file-missing"] = "true";
Provider.of<FlwtchState>(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "file-downloaded", "false");
Provider.of<FlwtchState>(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", "");
Provider.of<FlwtchState>(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "file-missing", "true");
} else {
Provider.of<MessageMetadata>(context).attributes["file-missing"] = "false";
Provider.of<FlwtchState>(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "file-missing", "false");
}
});
}
}
@ -94,6 +126,12 @@ class FileBubbleState extends State<FileBubble> {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
// we don't preview a non downloaded file...
if (widget.isPreview && myFile != null) {
return getPreview(context);
}
return LayoutBuilder(builder: (bcontext, constraints) {
var wdgSender = Visibility(
visible: widget.interactive,
@ -118,21 +156,7 @@ class FileBubbleState extends State<FileBubble> {
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
child: Padding(
padding: EdgeInsets.all(1.0),
child: Image.file(
myFile!,
cacheWidth: 2048,
// limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(bcontext).size.height * 0.30,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
)),
child: Padding(padding: EdgeInsets.all(1.0), child: getPreview(context)),
onTap: () {
pop(bcontext, myFile!, wdgMessage);
},
@ -167,7 +191,9 @@ class FileBubbleState extends State<FileBubble> {
}
} else if (!senderIsContact) {
wdgDecorations = Text(AppLocalizations.of(context)!.msgAddToAccept);
} else if (!widget.isAuto) {
} else if (!widget.isAuto || Provider.of<MessageMetadata>(context).attributes["file-missing"] == "false") {
//Note: we need this second case to account for scenarios where a user deletes the downloaded file, we won't automatically
// fetch it again, so we need to offer the user the ability to restart..
wdgDecorations = Visibility(
visible: widget.interactive,
child: Center(
@ -185,10 +211,10 @@ class FileBubbleState extends State<FileBubble> {
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor,
border: Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor, width: 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(borderRadiousEh),
topRight: Radius.circular(borderRadiousEh),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
topLeft: Radius.circular(borderRadius),
topRight: Radius.circular(borderRadius),
bottomLeft: fromMe ? Radius.circular(borderRadius) : Radius.zero,
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadius),
),
),
child: Padding(

View File

@ -1,16 +1,13 @@
import 'dart:io';
import 'package:cwtch/controllers/open_link_modal.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart';
import 'messagebubbledecorations.dart';
@ -55,7 +52,7 @@ class MessageBubbleState extends State<MessageBubble> {
linkifiers: [UrlLinkifier()],
onOpen: showClickableLinks
? (link) {
_modalOpenLink(context, link);
modalOpenLink(context, link);
}
: null,
//key: Key(myKey),
@ -63,9 +60,11 @@ class MessageBubbleState extends State<MessageBubble> {
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
linkStyle: TextStyle(
color: Provider.of<Settings>(context).current().mainTextColor,
),
linkStyle: TextStyle(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor),
codeStyle: TextStyle(
// note: these colors are flipped
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherTextColor : Provider.of<Settings>(context).theme.messageFromMeTextColor,
backgroundColor: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
@ -102,59 +101,4 @@ class MessageBubbleState extends State<MessageBubble> {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
});
}
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context)!.clickableLinksWarning),
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.clickableLinksCopy, semanticsLabel: AppLocalizations.of(context)!.clickableLinksCopy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: link.url));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification),
);
Navigator.pop(bcontext);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.clickableLinkOpen, semanticsLabel: AppLocalizations.of(context)!.clickableLinkOpen),
onPressed: () async {
if (await canLaunch(link.url)) {
await launch(link.url);
Navigator.pop(bcontext);
} else {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.clickableLinkError),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
},
),
),
]),
],
)),
));
});
}
}

View File

@ -1,6 +1,7 @@
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messagecache.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/material.dart';
@ -8,12 +9,12 @@ import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../settings.dart';
class MessageList extends StatefulWidget {
ItemScrollController scrollController;
ItemPositionsListener scrollListener;
MessageList(this.scrollController, this.scrollListener);
MessageList(this.scrollListener);
@override
_MessageListState createState() => _MessageListState();
@ -22,6 +23,12 @@ class MessageList extends StatefulWidget {
class _MessageListState extends State<MessageList> {
@override
Widget build(BuildContext outerContext) {
// On Android we can have unsynced messages at the front of the index from when the UI was asleep, if there are some, kick off sync of those first
if (Provider.of<ContactInfoState>(context).messageCache.indexUnsynced != 0) {
var conversationId = Provider.of<AppState>(outerContext, listen: false).selectedConversation!;
MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
}
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
@ -73,7 +80,7 @@ class _MessageListState extends State<MessageList> {
child: loadMessages
? ScrollablePositionedList.builder(
itemPositionsListener: widget.scrollListener,
itemScrollController: widget.scrollController,
itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController,
initialScrollIndex: initi > 4 ? initi - 4 : 0,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
@ -91,7 +98,7 @@ class _MessageListState extends State<MessageList> {
// reliably use this without running into duplicate keys...it isn't ideal as it means keys need to be re-built
// when new messages are added...however it is better than the alternative of not having widget keys at all.
var key = Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, messageIndex);
return message.getWidget(context, key);
return message.getWidget(context, key, messageIndex);
} else {
return MessageLoadingBubble();
}

View File

@ -18,8 +18,9 @@ import '../settings.dart';
class MessageRow extends StatefulWidget {
final Widget child;
final int index;
MessageRow(this.child, {Key? key}) : super(key: key);
MessageRow(this.child, this.index, {Key? key}) : super(key: key);
@override
MessageRowState createState() => MessageRowState();
@ -32,12 +33,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
late Alignment _dragAlignment = Alignment.center;
Alignment _dragAffinity = Alignment.center;
late int index;
@override
void initState() {
super.initState();
index = Provider.of<MessageMetadata>(context, listen: false).messageID;
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
@ -224,8 +222,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
children: widgetRow,
)))));
var markMsgId = Provider.of<ContactInfoState>(context).newMarkerMsgId;
if (markMsgId == Provider.of<MessageMetadata>(context).messageID) {
if (Provider.of<ContactInfoState>(context).newMarkerMsgIndex == widget.index) {
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
} else {
return mr;

View File

@ -38,7 +38,7 @@ class _ProfileImageState extends State<ProfileImage> {
var file = new File(widget.imagePath);
var image = Image.file(
file,
cacheWidth: 1920,
cacheWidth: (4 * widget.diameter.floor()),
filterQuality: FilterQuality.medium,
fit: BoxFit.cover,
alignment: Alignment.center,

View File

@ -1,8 +1,13 @@
import 'package:cwtch/controllers/open_link_modal.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
import 'package:cwtch/views/contactsview.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
@ -43,12 +48,29 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor));
var wdgMessage = SelectableText(
widget.body + '\u202F',
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
var formatMessages = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var wdgMessage = SelectableLinkify(
text: widget.body + '\u202F',
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
options: LinkifyOptions(messageFormatting: formatMessages, parseLinks: showClickableLinks, looseUrl: true, defaultToHttps: true),
linkifiers: [UrlLinkifier()],
onOpen: showClickableLinks
? (link) {
modalOpenLink(context, link);
}
: null,
//key: Key(myKey),
focusNode: _focus,
style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor,
),
linkStyle: TextStyle(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor),
codeStyle: TextStyle(
// note: these colors are flipped
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherTextColor : Provider.of<Settings>(context).theme.messageFromMeTextColor,
backgroundColor: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
@ -61,14 +83,31 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
var qMessage = (snapshot.data! as Message);
// Swap the background color for quoted tweets..
var qTextColor = fromMe ? Provider.of<Settings>(context).theme.messageFromOtherTextColor : Provider.of<Settings>(context).theme.messageFromMeTextColor;
return Container(
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
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(Icons.reply, size: 32, color: qTextColor))),
Center(widthFactor: 1.0, child: DefaultTextStyle(child: qMessage.getPreviewWidget(context), style: TextStyle(color: qTextColor)))
]));
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
var index = Provider.of<ContactInfoState>(context, listen: false).messageCache.cacheByHash[qMessage.getMetadata().contenthash];
var totalMessages = Provider.of<ContactInfoState>(context, listen: false).totalMessages;
// we have to reverse here because the list itself is reversed...
Provider.of<ContactInfoState>(context).messageScrollController.scrollTo(index: totalMessages - index!, duration: Duration(milliseconds: 100));
},
child: Container(
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(),
height: 75,
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [
Padding(padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Icon(Icons.reply, size: 32, color: qTextColor)),
DefaultTextStyle(
textWidthBasis: TextWidthBasis.parent,
child: qMessage.getPreviewWidget(context),
style: TextStyle(color: qTextColor),
overflow: TextOverflow.fade,
)
]))));
} catch (e) {
print(e);
return MalformedBubble();

View File

@ -190,6 +190,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.1"
desktop_notifications:
dependency: "direct main"
description:
name: desktop_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
fake_async:
dependency: transitive
description:

View File

@ -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.6.2+27
version: 1.7.1+29
environment:
sdk: ">=2.15.0 <3.0.0"
@ -42,8 +42,10 @@ dependencies:
file_picker_desktop: ^1.1.0
url_launcher: ^6.0.18
window_manager: ^0.1.4
# notification plugins
win_toast: ^0.0.2
flutter_local_notifications: 9.3.2
desktop_notifications: ^0.6.3
dev_dependencies:
msix: ^2.1.3

View File

@ -50,7 +50,10 @@ ShowInstDetails show
!define MUI_WELCOMEPAGE_TITLE "Welcome to the Cwtch installer"
!define MUI_WELCOMEPAGE_TEXT "Cwtch (pronounced: kutch) is a Welsh word roughly meaning 'a hug that creates a safe space'$\n$\n\
Cwtch is a platform for building consentful, decentralized, untrusted infrastructure using metadata resistant group communication applications. Currently there is a selfnamed instant messaging prototype app that is driving development and testing. Many Further apps are planned as the platform matures."
Cwtch is a platform for building consentful, decentralized, untrusted infrastructure using metadata resistant group communication applications. Currently there is a selfnamed instant messaging prototype app that is driving development and testing. Many Further apps are planned as the platform matures.$\n$\n\
Please close any running copies of Cwtch before installing a new version."
; Detecting if Cwtch is running and reminding the user or closing it appears to require 3rd party plugins that take the form of decade+ old .dlls in zips from a wiki...
!define MUI_FINISHPAGE_TITLE "Enjoy Cwtch"
!define MUI_FINISHPAGE_RUN $INSTDIR/cwtch.exe
@ -96,10 +99,16 @@ Section
WriteUninstaller "$INSTDIR\uninstall.exe"
# https://nsis.sourceforge.io/Add_uninstall_information_to_Add/Remove_Programs
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cwtch" \
"DisplayName" "Cwtch"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cwtch" \
"UninstallString" "$\"$INSTDIR\uninstall.exe$\""
SectionEnd
Section "Uninstall"
RMDir /r /REBOOTOK "$INSTDIR"
DeleteRegKey /ifempty HKCU "Software\Cwtch\installLocation"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cwtch"
SectionEnd