forked from cwtch.im/cwtch-ui
Compare commits
243 Commits
Author | SHA1 | Date |
---|---|---|
Kaio Duarte Costa | d4a87cd416 | |
Kaio Duarte Costa | 0455ed15d7 | |
Kaio Duarte Costa | 1f15e8af39 | |
Kaio Duarte Costa | 32b4ad2576 | |
Sarah Jamie Lewis | 08d337401f | |
Sarah Jamie Lewis | 91eca10f12 | |
Sarah Jamie Lewis | 870e7338ae | |
Sarah Jamie Lewis | af4aab3a47 | |
Dan Ballard | 8972d0eef5 | |
Sarah Jamie Lewis | 881cfbd0a3 | |
Sarah Jamie Lewis | c5f684e42e | |
Dan Ballard | 2d89b30881 | |
Sarah Jamie Lewis | e51c30ecc9 | |
Sarah Jamie Lewis | fb4c438e1c | |
Sarah Jamie Lewis | 2eca5058a8 | |
Dan Ballard | 94297ee85f | |
Sarah Jamie Lewis | 3fe732809d | |
Dan Ballard | 52b1f28252 | |
Dan Ballard | 16f413177f | |
Dan Ballard | ea7b307de2 | |
Dan Ballard | 4d4901838e | |
Dan Ballard | cfa4b4f95b | |
Dan Ballard | 2defc7ea2c | |
Sarah Jamie Lewis | cfb32bc84a | |
Dan Ballard | e570f6941b | |
Sarah Jamie Lewis | 37e18d03a1 | |
Sarah Jamie Lewis | 76c925d874 | |
Dan Ballard | 975983be3c | |
Sarah Jamie Lewis | 7edc46743f | |
erinn | 74ab39067e | |
erinn | cac2064731 | |
Dan Ballard | 93284708e0 | |
Sarah Jamie Lewis | 521c0600a2 | |
Dan Ballard | 7f2a8d649d | |
Dan Ballard | d550c23fbd | |
Dan Ballard | 5d09341277 | |
Dan Ballard | 191065f51c | |
Sarah Jamie Lewis | 47e26f18fc | |
Dan Ballard | 2bf28e2c6a | |
Sarah Jamie Lewis | af3c6940bd | |
Dan Ballard | c3fa6735f5 | |
Sarah Jamie Lewis | 8cfbc39988 | |
Sarah Jamie Lewis | fc29b10f12 | |
Dan Ballard | a49ad07b40 | |
Sarah Jamie Lewis | 34e296959a | |
Sarah Jamie Lewis | d4d7a54af1 | |
Sarah Jamie Lewis | 5139846f31 | |
Sarah Jamie Lewis | 483213c63b | |
Sarah Jamie Lewis | 546ac6c23d | |
Dan Ballard | 3a752b7397 | |
Sarah Jamie Lewis | 7540aed701 | |
Sarah Jamie Lewis | ad52f2e0c8 | |
Sarah Jamie Lewis | 337f6dc5d9 | |
Sarah Jamie Lewis | 814e6df6f6 | |
Dan Ballard | 62ea8278f3 | |
Sarah Jamie Lewis | e8a638ed29 | |
Sarah Jamie Lewis | 44fba12d21 | |
Sarah Jamie Lewis | 7516232bd4 | |
Sarah Jamie Lewis | 5be25b87c4 | |
Sarah Jamie Lewis | e13ad5d218 | |
Sarah Jamie Lewis | c397a9cdb7 | |
Sarah Jamie Lewis | 60e822cf12 | |
Sarah Jamie Lewis | 0ea2a2116e | |
Dan Ballard | 9fb9759e6a | |
Sarah Jamie Lewis | 62b87f2939 | |
Sarah Jamie Lewis | da58555104 | |
Sarah Jamie Lewis | d8cfb5c730 | |
Sarah Jamie Lewis | 0dd9ecedac | |
Sarah Jamie Lewis | 61ee9491ab | |
Sarah Jamie Lewis | 6b9cf1f164 | |
Sarah Jamie Lewis | b8326762bf | |
Dan Ballard | af9a386ae8 | |
Sarah Jamie Lewis | da5925c7b3 | |
Dan Ballard | eabee61687 | |
Dan Ballard | 102341f931 | |
Dan Ballard | e36c5bf2f9 | |
Dan Ballard | 629c9152ca | |
Dan Ballard | 9298be0a61 | |
Sarah Jamie Lewis | fc4a87e3aa | |
Dan Ballard | bef8ca083b | |
Dan Ballard | 8d0b277731 | |
Sarah Jamie Lewis | 7badbca926 | |
Dan Ballard | 708f00f678 | |
Dan Ballard | 4404977128 | |
Sarah Jamie Lewis | e29366cb49 | |
Sarah Jamie Lewis | 3a12a94a85 | |
Sarah Jamie Lewis | 8da9db87de | |
Sarah Jamie Lewis | 93adb32ca5 | |
Dan Ballard | ee9af54917 | |
Sarah Jamie Lewis | 453feae88a | |
Dan Ballard | e32e32ed27 | |
Sarah Jamie Lewis | bf1eece1e2 | |
Sarah Jamie Lewis | 9c9916e7c9 | |
Sarah Jamie Lewis | b3788b4f05 | |
Dan Ballard | 5f67f626e5 | |
Sarah Jamie Lewis | 00ca54a6a3 | |
Sarah Jamie Lewis | 5770eb4b66 | |
Sarah Jamie Lewis | ab77ad80d1 | |
Dan Ballard | 0c426a129a | |
Sarah Jamie Lewis | 0aa0d286ef | |
Sarah Jamie Lewis | 1483ddcc94 | |
Dan Ballard | 405160947b | |
Dan Ballard | 3a5668734e | |
Dan Ballard | a6406e9068 | |
Sarah Jamie Lewis | a4e1a7ede1 | |
Sarah Jamie Lewis | 77227111fd | |
Sarah Jamie Lewis | 9d2654459c | |
Sarah Jamie Lewis | 6dca8e80e6 | |
Dan Ballard | 3f4530f299 | |
Dan Ballard | 40b3207e2d | |
Sarah Jamie Lewis | fc1f910486 | |
Sarah Jamie Lewis | f71bce5b71 | |
Sarah Jamie Lewis | c01860f1de | |
Dan Ballard | c7e6cfcbc1 | |
Dan Ballard | d9acca7b1b | |
Dan Ballard | 914fe9c300 | |
Dan Ballard | ce5499419f | |
Dan Ballard | 531595e9e9 | |
Dan Ballard | 9857dff9a3 | |
Dan Ballard | 03b3d86a41 | |
Dan Ballard | a5040b7236 | |
Dan Ballard | a83b357f0f | |
Sarah Jamie Lewis | b425175fff | |
Sarah Jamie Lewis | 8570199196 | |
Dan Ballard | 1122c818f5 | |
Dan Ballard | 6714b0d8a0 | |
Sarah Jamie Lewis | 0d90219c87 | |
Sarah Jamie Lewis | 8ab82569e3 | |
Dan Ballard | a7861681e1 | |
Sarah Jamie Lewis | 106b45c758 | |
Sarah Jamie Lewis | 644ae502e5 | |
Sarah Jamie Lewis | 7bae6485f7 | |
Sarah Jamie Lewis | 04c335e7a4 | |
Sarah Jamie Lewis | 3961692817 | |
Sarah Jamie Lewis | d703a9636f | |
Dan Ballard | e4419366a4 | |
Sarah Jamie Lewis | f848316db9 | |
Sarah Jamie Lewis | a5b253f185 | |
Sarah Jamie Lewis | e7c19c7477 | |
Dan Ballard | 59df024867 | |
Sarah Jamie Lewis | 65ff084952 | |
Sarah Jamie Lewis | b3e11cfffd | |
Sarah Jamie Lewis | 0c9be47e17 | |
Dan Ballard | 3bb3a8736c | |
Sarah Jamie Lewis | 67850e8e4b | |
Dan Ballard | c8e896fa51 | |
Sarah Jamie Lewis | d1e8f71290 | |
Sarah Jamie Lewis | be8646e805 | |
Sarah Jamie Lewis | 6d42f2c76c | |
Sarah Jamie Lewis | 8429907650 | |
Sarah Jamie Lewis | c3848553d7 | |
Sarah Jamie Lewis | 3c85b8f59e | |
Sarah Jamie Lewis | d0e7e6703b | |
Sarah Jamie Lewis | 2bc47173f9 | |
Sarah Jamie Lewis | 15c68d8812 | |
Sarah Jamie Lewis | e76f2883c6 | |
Dan Ballard | 439b9b874f | |
Sarah Jamie Lewis | f5393cdb79 | |
Sarah Jamie Lewis | c0f1b674aa | |
Dan Ballard | 630713a5e4 | |
Sarah Jamie Lewis | d10a6df872 | |
Sarah Jamie Lewis | 2723a35d44 | |
Dan Ballard | 427081c937 | |
Sarah Jamie Lewis | 9d4abc3725 | |
Sarah Jamie Lewis | fa52b741bf | |
Dan Ballard | fb86fb6eae | |
Sarah Jamie Lewis | 8dd696b6ab | |
Dan Ballard | 001ad854c7 | |
Dan Ballard | af5fb678fc | |
Dan Ballard | ffa51e83a1 | |
Dan Ballard | 441845ed49 | |
Sarah Jamie Lewis | 0146436cb3 | |
Dan Ballard | 0647a2d98d | |
Dan Ballard | 0bcfe75a63 | |
Dan Ballard | ecdcef2192 | |
Dan Ballard | e6c9f7becb | |
Sarah Jamie Lewis | 9d8f73ac00 | |
Sarah Jamie Lewis | dc78117e1a | |
Dan Ballard | 59e3220bce | |
Sarah Jamie Lewis | 653ba199bc | |
Sarah Jamie Lewis | 1b45205c48 | |
Dan Ballard | 85186b2565 | |
Sarah Jamie Lewis | 3287fa79ff | |
Sarah Jamie Lewis | 111d522484 | |
Sarah Jamie Lewis | 20c854bafb | |
Dan Ballard | ffdc7b3262 | |
Dan Ballard | a3d986d9d6 | |
Sarah Jamie Lewis | 5e3387ec8a | |
Dan Ballard | a6c7682c84 | |
Dan Ballard | b29836ed3b | |
Sarah Jamie Lewis | e0bf47b6ab | |
Dan Ballard | 4bd92d854f | |
Dan Ballard | 82d1bf873f | |
Dan Ballard | 5959981fe4 | |
Dan Ballard | ab315e289a | |
Dan Ballard | 6392d67332 | |
Dan Ballard | 8f0b73af2a | |
Dan Ballard | 4e2f83ccd9 | |
Dan Ballard | dc5ba7b392 | |
Sarah Jamie Lewis | 3595f5d8d1 | |
Sarah Jamie Lewis | 1df348c0c1 | |
Sarah Jamie Lewis | 548e7f4925 | |
Dan Ballard | a20d2dffc4 | |
Dan Ballard | 2a712565e9 | |
Dan Ballard | a94fd3547b | |
Dan Ballard | c377a09748 | |
Dan Ballard | d261fbd4c0 | |
Dan Ballard | 933ca74fbc | |
Sarah Jamie Lewis | 38f317194d | |
Sarah Jamie Lewis | a4ab2ec060 | |
Dan Ballard | 47795094a0 | |
Sarah Jamie Lewis | 0d1e7bb5a0 | |
Sarah Jamie Lewis | 987b80c92b | |
Sarah Jamie Lewis | e718adad8a | |
Sarah Jamie Lewis | 0b9c159e85 | |
Sarah Jamie Lewis | a4a2af08b4 | |
Sarah Jamie Lewis | 471a729d46 | |
Dan Ballard | 1cffea5c1a | |
Sarah Jamie Lewis | e7c5b2cfa5 | |
Dan Ballard | e08114881c | |
Sarah Jamie Lewis | 6eaf95a33b | |
Dan Ballard | 0db68bcdbb | |
Dan Ballard | f64559191b | |
Dan Ballard | b8c1c7682b | |
Dan Ballard | 9812111041 | |
Dan Ballard | ecc9a3a48c | |
Dan Ballard | 523531e6be | |
Dan Ballard | ff3e60a750 | |
Dan Ballard | 5a1c66bc25 | |
Sarah Jamie Lewis | 10780ac8cb | |
Sarah Jamie Lewis | 0857d46809 | |
Dan Ballard | d7d3b2ef97 | |
Sarah Jamie Lewis | 65d5e9777d | |
Dan Ballard | 27f4c5f00e | |
Sarah Jamie Lewis | f48b6af3dd | |
Dan Ballard | d8e19de5b1 | |
Sarah Jamie Lewis | af03dd30cc | |
Sarah Jamie Lewis | 8a3867b5b3 | |
Sarah Jamie Lewis | 6237032716 | |
Dan Ballard | 915cf1a6d8 | |
Dan Ballard | c4ebed0a71 | |
Dan Ballard | 3c71bb8184 | |
Sarah Jamie Lewis | c3661d4caa |
24
.drone.yml
24
.drone.yml
|
@ -8,7 +8,7 @@ clone:
|
|||
|
||||
steps:
|
||||
- name: clone
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
|
@ -24,7 +24,7 @@ steps:
|
|||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -47,7 +47,7 @@ steps:
|
|||
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
|
||||
|
||||
- name: build-linux
|
||||
image: openpriv/flutter-desktop:linux-fstable-2.8.0
|
||||
image: openpriv/flutter-desktop:linux-fstable-3.3.8
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -61,7 +61,7 @@ steps:
|
|||
- rm -r cwtch
|
||||
|
||||
- name: test-build-android
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
when:
|
||||
event: pull_request
|
||||
volumes:
|
||||
|
@ -71,7 +71,7 @@ steps:
|
|||
- flutter build apk --debug
|
||||
|
||||
- name: build-android
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
when:
|
||||
event: push
|
||||
environment:
|
||||
|
@ -95,7 +95,7 @@ steps:
|
|||
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
|
||||
|
||||
- name: widget-tests
|
||||
image: cirrusci/flutter:2.8.0
|
||||
image: cirrusci/flutter:3.3.8
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -177,7 +177,7 @@ clone:
|
|||
|
||||
steps:
|
||||
- name: clone
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
|
@ -195,7 +195,7 @@ steps:
|
|||
- git checkout $Env:DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
commands:
|
||||
- git describe --tags --abbrev=1 > VERSION
|
||||
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE
|
||||
|
@ -203,7 +203,7 @@ steps:
|
|||
- .\fetch-libcwtch-go.ps1
|
||||
|
||||
- name: build-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
commands:
|
||||
- flutter pub get
|
||||
- $Env:version += type .\VERSION
|
||||
|
@ -229,7 +229,7 @@ steps:
|
|||
status: [ success ]
|
||||
environment:
|
||||
pfx:
|
||||
from_secret: pfx
|
||||
from_secret: pfx2022_b64
|
||||
pfx_pass:
|
||||
from_secret: pfx_pass
|
||||
commands:
|
||||
|
@ -245,6 +245,8 @@ steps:
|
|||
- echo $Env:pfx > codesign.pfx.b64
|
||||
- certutil -decode codesign.pfx.b64 codesign.pfx
|
||||
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\cwtch.exe
|
||||
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\libCwtch.dll
|
||||
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\flutter_windows.dll
|
||||
- copy windows\runner\resources\knot_128.ico $Env:releasedir\cwtch.ico
|
||||
- makensis windows\nsis\cwtch-installer.nsi
|
||||
- move windows\nsis\cwtch-installer.exe cwtch-installer.exe
|
||||
|
@ -260,7 +262,7 @@ steps:
|
|||
- move *.sha512 deploy\$Env:builddir
|
||||
|
||||
- name: deploy-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable-3.3.8
|
||||
when:
|
||||
event: push
|
||||
status: [ success ]
|
||||
|
|
|
@ -40,10 +40,17 @@ app.*.symbols
|
|||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Tor
|
||||
data-dir*
|
||||
|
||||
|
||||
# Compiled Libs
|
||||
linux/tor
|
||||
linux/libCwtch.so
|
||||
android/cwtch/cwtch.aar
|
||||
android/app/src/main/jniLibs/*/libtor.so
|
||||
libCwtch.dylib
|
||||
|
||||
coverage
|
||||
test/failures
|
||||
.gradle
|
||||
|
|
|
@ -1 +1 @@
|
|||
2022-03-03-19-53-v1.6.0-4-g4b881b9
|
||||
2022-09-10-15-31-v1.9.0-6-g397d264
|
|
@ -1 +1 @@
|
|||
2022-03-04-00-54-v1.6.0-4-g4b881b9
|
||||
2022-09-10-19-30-v1.9.0-6-g397d264
|
|
@ -65,7 +65,7 @@ To build a release version and load normal profiles, use `build-release.sh X` in
|
|||
### Building on MacOS
|
||||
|
||||
- Cocaopods is required, you may need to `gem install cocaopods -v 1.9.3`
|
||||
- copy `libCwtch.dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it
|
||||
- copy `libCwtch.x64.dylib` and `libCwtch.arm/dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it
|
||||
- run `fetch-tor-macos.sh` to fetch Tor or Download and install Tor Browser and `cp -r /Applications/Tor\ Browser.app/Contents/MacOS/Tor ./macos/`
|
||||
- `flutter build macos`
|
||||
- optional: launch cwtch-ui release build with `./build/macos/Build/Products/Release/Cwtch.app/Contents/MacOS/Cwtch`
|
||||
|
|
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
@ -48,7 +48,7 @@ android {
|
|||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "im.cwtch.flwtch"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 31
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
@ -99,7 +99,10 @@ dependencies {
|
|||
//implementation("androidx.work:work-runtime:$work_version")
|
||||
|
||||
// Kotlin + coroutines
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
// 2022.06: upgraded from 2.5 to 2.7 for android 12
|
||||
// err: "requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent"
|
||||
// as per https://github.com/flutter/flutter/issues/93609
|
||||
implementation 'androidx.work:work-runtime-ktx:2.7.0'
|
||||
|
||||
// optional - RxJava2 support
|
||||
//implementation("androidx.work:work-rxjava2:$work_version")
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
android:theme="@style/NormalTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
|
@ -46,7 +47,15 @@
|
|||
<!--Needed to run in background (lol)-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<!-- 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" />
|
||||
|
||||
<queries>
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
// Generated file.
|
||||
//
|
||||
// If you wish to remove Flutter's multidex support, delete this entire file.
|
||||
//
|
||||
// Modifications to this file should be done in a copy under a different name
|
||||
// as this file may be regenerated.
|
||||
|
||||
package io.flutter.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.multidex.MultiDex;
|
||||
|
||||
/**
|
||||
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
|
||||
* Extension of {@link android.app.Application}, adding multidex support.
|
||||
*/
|
||||
public class FlutterMultiDexApplication extends FlutterApplication {
|
||||
public class FlutterMultiDexApplication extends Application {
|
||||
@Override
|
||||
@CallSuper
|
||||
protected void attachBaseContext(Context base) {
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
package im.cwtch.flwtch
|
||||
|
||||
import android.app.*
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.*
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import cwtch.Cwtch
|
||||
import io.flutter.FlutterInjector
|
||||
import org.json.JSONObject
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import android.net.Uri
|
||||
|
||||
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||
CoroutineWorker(context, parameters) {
|
||||
|
@ -32,6 +35,8 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
private var notificationSimple: String? = null
|
||||
private var notificationConversationInfo: String? = null
|
||||
|
||||
private val TAG: String = "FlwtchWorker.kt"
|
||||
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
// Hack to uncomment and deploy if your device has zombie workers you need to kill
|
||||
|
@ -60,23 +65,34 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
}
|
||||
|
||||
private fun handleCwtch(method: String, args: String): Result {
|
||||
if (method != "Start") {
|
||||
if (Cwtch.started() != 1.toLong()) {
|
||||
Log.e(TAG, "libCwtch-go reports it is not initialized yet")
|
||||
return Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
val a = JSONObject(args)
|
||||
when (method) {
|
||||
"Start" -> {
|
||||
Log.i("FlwtchWorker.kt", "handleAppInfo Start")
|
||||
Log.i(TAG, "handleAppInfo Start")
|
||||
val appDir = (a.get("appDir") as? String) ?: ""
|
||||
val torPath = (a.get("torPath") as? String) ?: "tor"
|
||||
Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'")
|
||||
Log.i(TAG, "appDir: '$appDir' torPath: '$torPath'")
|
||||
|
||||
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
|
||||
|
||||
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||
Log.i(TAG, "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||
val downloadIDs = mutableMapOf<String, Int>()
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
while (true) {
|
||||
try {
|
||||
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
||||
// TODO replace this notification block with the NixNotification manager in dart as it has access to contact names and also needs less working around
|
||||
|
||||
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
|
||||
val data = JSONObject(evt.Data)
|
||||
val handle = data.getString("RemotePeer");
|
||||
|
@ -99,12 +115,11 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
intent.action = Intent.ACTION_RUN
|
||||
intent.putExtra("EventType", "NotificationClicked")
|
||||
}
|
||||
|
||||
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle("Cwtch")
|
||||
.setContentText(notificationSimple ?: "New Message")
|
||||
.setSmallIcon(R.mipmap.knott_transparent)
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
|
@ -119,8 +134,8 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
""
|
||||
}
|
||||
val loader = FlutterInjector.instance().flutterLoader()
|
||||
Log.i("FlwtchWorker.kt", "notification for " + evt.EventType + " " + handle + " " + conversationId + " " + channelId)
|
||||
Log.i("FlwtchWorker.kt", data.toString());
|
||||
Log.i(TAG, "notification for " + evt.EventType + " " + handle + " " + conversationId + " " + channelId)
|
||||
Log.i(TAG, data.toString());
|
||||
val key = loader.getLookupKeyForAsset(data.getString("picture"))//"assets/profiles/001-centaur.png")
|
||||
val fh = applicationContext.assets.open(key)
|
||||
|
||||
|
@ -137,7 +152,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
?: "New Message From %1").replace("%1", data.getString("Nick")))
|
||||
.setLargeIcon(BitmapFactory.decodeStream(fh))
|
||||
.setSmallIcon(R.mipmap.knott_transparent)
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, flags))
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
|
@ -183,18 +198,18 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
|
||||
}
|
||||
} else if (evt.EventType == "FileDownloaded") {
|
||||
Log.d("FlwtchWorker", "file downloaded!");
|
||||
Log.d(TAG, "file downloaded!");
|
||||
val data = JSONObject(evt.Data);
|
||||
val tempFile = data.getString("TempFile");
|
||||
val fileKey = data.getString("FileKey");
|
||||
if (tempFile != "" && tempFile != data.getString("FilePath")) {
|
||||
val filePath = data.getString("FilePath");
|
||||
Log.i("FlwtchWorker", "moving " + tempFile + " to " + filePath);
|
||||
Log.i(TAG, "moving " + tempFile + " to " + filePath);
|
||||
val sourcePath = Paths.get(tempFile);
|
||||
val targetUri = Uri.parse(filePath);
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
|
||||
val bytesWritten = Files.copy(sourcePath, os);
|
||||
Log.d("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
|
||||
Log.d("TAG", "copied " + bytesWritten.toString() + " bytes");
|
||||
if (bytesWritten != 0L) {
|
||||
os?.flush();
|
||||
os?.close();
|
||||
|
@ -213,219 +228,26 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
intent.putExtra("EventID", evt.EventID)
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
||||
}
|
||||
if (evt.EventType == "Shutdown") {
|
||||
Log.i(TAG, "processing shutdown event, exiting FlwtchWorker/Start()...");
|
||||
return Result.success()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("FlwtchWorker", "Error in handleCwtch: " + e.toString() + " :: " + e.getStackTrace());
|
||||
Log.e(TAG, "Error in handleCwtch: " + e.toString() + " :: " + e.getStackTrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"ReconnectCwtchForeground" -> {
|
||||
Cwtch.reconnectCwtchForeground()
|
||||
}
|
||||
"CreateProfile" -> {
|
||||
val nick = (a.get("nick") as? String) ?: ""
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.createProfile(nick, pass)
|
||||
}
|
||||
"LoadProfiles" -> {
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.loadProfiles(pass)
|
||||
}
|
||||
"ChangePassword" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val pass = (a.get("OldPass") as? String) ?: ""
|
||||
val passNew = (a.get("NewPass") as? String) ?: ""
|
||||
val passNew2 = (a.get("NewPassAgain") as? String) ?: ""
|
||||
Cwtch.changePassword(profile, pass, passNew, passNew2)
|
||||
}
|
||||
"GetMessage" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val indexI = a.getInt("index").toLong()
|
||||
Log.d("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString())
|
||||
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build())
|
||||
}
|
||||
"GetMessageByID" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val id = a.getInt("id").toLong()
|
||||
return Result.success(Data.Builder().putString("result", Cwtch.getMessageByID(profile, conversation, id)).build())
|
||||
}
|
||||
"GetMessageByContentHash" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val contentHash = (a.get("contentHash") as? String) ?: ""
|
||||
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, conversation, contentHash)).build())
|
||||
}
|
||||
"UpdateMessageAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val channel = a.getInt("chanenl").toLong()
|
||||
val midx = a.getInt("midx").toLong()
|
||||
val key = (a.get("key") as? String) ?: ""
|
||||
val value = (a.get("value") as? String) ?: ""
|
||||
Cwtch.setMessageAttribute(profile, conversation, channel, midx, key, value)
|
||||
}
|
||||
"AcceptConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.acceptConversation(profile, conversation)
|
||||
}
|
||||
"BlockContact" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.blockContact(profile, conversation)
|
||||
}
|
||||
"UnblockContact" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.unblockContact(profile, conversation)
|
||||
}
|
||||
"SendMessage" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val message = (a.get("message") as? String) ?: ""
|
||||
Log.i("FlwtchWorker.kt", "SendMessage: $message")
|
||||
Cwtch.sendMessage(profile, conversation, message)
|
||||
}
|
||||
"SendInvitation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val target = a.getInt("target").toLong()
|
||||
Cwtch.sendInvitation(profile, conversation, target)
|
||||
}
|
||||
"ShareFile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val filepath = (a.get("filepath") as? String) ?: ""
|
||||
Cwtch.shareFile(profile, conversation, filepath)
|
||||
}
|
||||
"DownloadFile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val filepath = (a.get("filepath") as? String) ?: ""
|
||||
val manifestpath = (a.get("manifestpath") as? String) ?: ""
|
||||
val filekey = (a.get("filekey") as? String) ?: ""
|
||||
// FIXME: Prevent spurious calls by Intent
|
||||
if (profile != "") {
|
||||
Cwtch.downloadFile(profile, conversation, filepath, manifestpath, filekey)
|
||||
}
|
||||
}
|
||||
"CheckDownloadStatus" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val fileKey = (a.get("fileKey") as? String) ?: ""
|
||||
Cwtch.checkDownloadStatus(profile, fileKey)
|
||||
}
|
||||
"VerifyOrResumeDownload" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val fileKey = (a.get("fileKey") as? String) ?: ""
|
||||
Cwtch.verifyOrResumeDownload(profile, conversation, fileKey)
|
||||
}
|
||||
"SendProfileEvent" -> {
|
||||
val onion = (a.get("onion") as? String) ?: ""
|
||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||
Cwtch.sendProfileEvent(onion, jsonEvent)
|
||||
}
|
||||
"SendAppEvent" -> {
|
||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||
Cwtch.sendAppEvent(jsonEvent)
|
||||
}
|
||||
"ResetTor" -> {
|
||||
Cwtch.resetTor()
|
||||
}
|
||||
"ImportBundle" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val bundle = (a.get("bundle") as? String) ?: ""
|
||||
Cwtch.importBundle(profile, bundle)
|
||||
}
|
||||
"CreateGroup" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val server = (a.get("server") as? String) ?: ""
|
||||
val groupName = (a.get("groupName") as? String) ?: ""
|
||||
Cwtch.createGroup(profile, server, groupName)
|
||||
}
|
||||
"DeleteProfile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.deleteProfile(profile, pass)
|
||||
}
|
||||
"ArchiveConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.archiveConversation(profile, conversation)
|
||||
}
|
||||
"DeleteConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
Cwtch.deleteContact(profile, conversation)
|
||||
}
|
||||
"SetProfileAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setProfileAttribute(profile, key, v)
|
||||
}
|
||||
"SetConversationAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setConversationAttribute(profile, conversation, key, v)
|
||||
}
|
||||
"Shutdown" -> {
|
||||
Cwtch.shutdownCwtch();
|
||||
return Result.success()
|
||||
}
|
||||
"LoadServers" -> {
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
Cwtch.loadServers(password)
|
||||
}
|
||||
"CreateServer" -> {
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
val desc = (a.get("Description") as? String) ?: ""
|
||||
val autostart = (a.get("Autostart") as? Boolean) ?: false
|
||||
Cwtch.createServer(password, desc, autostart)
|
||||
}
|
||||
"DeleteServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
Cwtch.deleteServer(serverOnion, password)
|
||||
}
|
||||
"LaunchServers" -> {
|
||||
Cwtch.launchServers()
|
||||
}
|
||||
"LaunchServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
Cwtch.launchServer(serverOnion)
|
||||
}
|
||||
"StopServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
Cwtch.stopServer(serverOnion)
|
||||
}
|
||||
"StopServers" -> {
|
||||
Cwtch.stopServers()
|
||||
}
|
||||
"DestroyServers" -> {
|
||||
Cwtch.destroyServers()
|
||||
}
|
||||
"SetServerAttribute" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setServerAttribute(serverOnion, key, v)
|
||||
}
|
||||
// Event passing translations from Flutter to Kotlin worker scope so the worker can use them
|
||||
"L10nInit" -> {
|
||||
notificationSimple = (a.get("notificationSimple") as? String) ?: "New Message"
|
||||
notificationConversationInfo = (a.get("notificationConversationInfo") as? String)
|
||||
?: "New Message From "
|
||||
}
|
||||
else -> {
|
||||
Log.i("FlwtchWorker", "unknown command: " + method);
|
||||
Log.i(TAG, "unknown command: " + method);
|
||||
return Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
|
@ -448,6 +270,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
intent.action = Intent.ACTION_RUN
|
||||
intent.putExtra("EventType", "ShutdownClicked")
|
||||
}
|
||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle(title)
|
||||
|
@ -457,7 +283,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
.setOngoing(true)
|
||||
// Add the cancel action to the notification which can
|
||||
// be used to cancel the worker
|
||||
.addAction(android.R.drawable.ic_delete, cancel, PendingIntent.getActivity(applicationContext, 2, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.addAction(android.R.drawable.ic_delete, cancel, PendingIntent.getActivity(applicationContext, 2, cancelIntent, flags))
|
||||
.build()
|
||||
|
||||
return ForegroundInfo(101, notification)
|
||||
|
|
|
@ -1,46 +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 java.util.concurrent.TimeUnit
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
|
@ -52,6 +54,7 @@ class MainActivity: FlutterActivity() {
|
|||
private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler"
|
||||
private val CHANNEL_SHUTDOWN_CLICK = "im.cwtch.flwtch/shutdownClickHandler"
|
||||
|
||||
private val TAG: String = "MainActivity.kt"
|
||||
// WorkManager tag applied to all Start() infinite coroutines
|
||||
val WORKER_TAG = "cwtchEventBusWorker"
|
||||
|
||||
|
@ -62,11 +65,27 @@ class MainActivity: FlutterActivity() {
|
|||
// "Download to..." prompt extra arguments
|
||||
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)
|
||||
|
@ -93,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;
|
||||
}
|
||||
|
||||
|
@ -110,8 +137,6 @@ class MainActivity: FlutterActivity() {
|
|||
)), ErrorLogResult(""));//placeholder; this Result is never actually invoked
|
||||
} else if (requestCode == PREVIEW_EXPORT_REQUEST_CODE) {
|
||||
val targetPath = intent!!.getData().toString()
|
||||
var srcFile = File(this.exportFromPath)
|
||||
Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file")
|
||||
val sourcePath = Paths.get(this.exportFromPath);
|
||||
val targetUri = Uri.parse(targetPath);
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
|
||||
|
@ -122,6 +147,20 @@ class MainActivity: FlutterActivity() {
|
|||
os?.close();
|
||||
//Files.delete(sourcePath);
|
||||
}
|
||||
} else if (requestCode == PROFILE_EXPORT_REQUEST_CODE ) {
|
||||
val targetPath = intent!!.getData().toString()
|
||||
val srcFile = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.exportFromPath).toString();
|
||||
Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file " + srcFile);
|
||||
val sourcePath = Paths.get(srcFile);
|
||||
val targetUri = Uri.parse(targetPath);
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
|
||||
val bytesWritten = Files.copy(sourcePath, os);
|
||||
Log.d("MainActivity:PREVIEW_EXPORT", "copied " + bytesWritten.toString() + " bytes");
|
||||
if (bytesWritten != 0L) {
|
||||
os?.flush();
|
||||
os?.close();
|
||||
//Files.delete(sourcePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,10 +171,13 @@ 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)
|
||||
private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
when (call.method) {
|
||||
CALL_APP_INFO -> result.success(getNativeLibDir())
|
||||
|
@ -144,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
|
||||
|
@ -154,76 +220,336 @@ class MainActivity: FlutterActivity() {
|
|||
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
|
||||
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
var method = call.method
|
||||
// todo change usage patern to match that in FlwtchWorker
|
||||
// Unsafe for anything using int args, causes access time attempt to cast to string which will fail
|
||||
val argmap: Map<String, String> = call.arguments as Map<String, String>
|
||||
|
||||
// the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start()
|
||||
// in case the ForegroundService is still running. in both cases, however, we *do* want to re-register
|
||||
// the eventbus listener.
|
||||
if (call.method == "Start") {
|
||||
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
|
||||
when (call.method) {
|
||||
"Start" -> {
|
||||
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
|
||||
|
||||
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
|
||||
// accidentally duplicated. however, we still need to manually check if it's running or not, so
|
||||
// that we can divert this method call to ReconnectCwtchForeground instead if so.
|
||||
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
|
||||
for (workInfo in works) {
|
||||
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
|
||||
}
|
||||
WorkManager.getInstance(this).pruneWork()
|
||||
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
|
||||
// accidentally duplicated. however, we still need to manually check if it's running or not, so
|
||||
// that we can divert this method call to ReconnectCwtchForeground instead if so.
|
||||
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
|
||||
for (workInfo in works) {
|
||||
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
|
||||
}
|
||||
WorkManager.getInstance(this).pruneWork()
|
||||
|
||||
Log.i("MainActivity.kt", "Start() launching foregroundservice")
|
||||
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
|
||||
// 15 minutes is the shortest interval you can request
|
||||
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
|
||||
return
|
||||
} else if (call.method == "CreateDownloadableFile") {
|
||||
this.dlToProfile = argmap["ProfileOnion"] ?: ""
|
||||
this.dlToHandle = argmap["handle"] ?: ""
|
||||
val suggestedName = argmap["filename"] ?: "filename.ext"
|
||||
this.dlToFileKey = argmap["filekey"] ?: ""
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/octet-stream"
|
||||
putExtra(Intent.EXTRA_TITLE, suggestedName)
|
||||
Log.i("MainActivity.kt", "Start() launching foregroundservice")
|
||||
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
|
||||
// 15 minutes is the shortest interval you can request
|
||||
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
|
||||
}
|
||||
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
|
||||
return
|
||||
} else if (call.method == "ExportPreviewedFile") {
|
||||
this.exportFromPath = argmap["Path"] ?: ""
|
||||
val suggestion = argmap["FileName"] ?: "filename.ext"
|
||||
var imgType = "jpeg"
|
||||
if (suggestion.endsWith("png")) {
|
||||
imgType = "png"
|
||||
} else if (suggestion.endsWith("webp")) {
|
||||
imgType = "webp"
|
||||
} else if (suggestion.endsWith("bmp")) {
|
||||
imgType = "bmp"
|
||||
} else if (suggestion.endsWith("gif")) {
|
||||
imgType = "gif"
|
||||
"CreateDownloadableFile" -> {
|
||||
this.dlToProfile = argmap["ProfileOnion"] ?: ""
|
||||
this.dlToHandle = argmap["handle"] ?: ""
|
||||
val suggestedName = argmap["filename"] ?: "filename.ext"
|
||||
this.dlToFileKey = argmap["filekey"] ?: ""
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/octet-stream"
|
||||
putExtra(Intent.EXTRA_TITLE, suggestedName)
|
||||
}
|
||||
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "image/" + imgType
|
||||
putExtra(Intent.EXTRA_TITLE, suggestion)
|
||||
"ExportPreviewedFile" -> {
|
||||
this.exportFromPath = argmap["Path"] ?: ""
|
||||
val suggestion = argmap["FileName"] ?: "filename.ext"
|
||||
var imgType = "jpeg"
|
||||
if (suggestion.endsWith("png")) {
|
||||
imgType = "png"
|
||||
} else if (suggestion.endsWith("webp")) {
|
||||
imgType = "webp"
|
||||
} else if (suggestion.endsWith("bmp")) {
|
||||
imgType = "bmp"
|
||||
} else if (suggestion.endsWith("gif")) {
|
||||
imgType = "gif"
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "image/" + imgType
|
||||
putExtra(Intent.EXTRA_TITLE, suggestion)
|
||||
}
|
||||
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
|
||||
}
|
||||
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
|
||||
return
|
||||
}
|
||||
"ExportProfile" -> {
|
||||
this.exportFromPath = argmap["file"] ?: ""
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/gzip"
|
||||
putExtra(Intent.EXTRA_TITLE, argmap["file"])
|
||||
}
|
||||
startActivityForResult(intent, PROFILE_EXPORT_REQUEST_CODE)
|
||||
}
|
||||
"GetMessages" -> {
|
||||
Log.d("MainActivity.kt", "Cwtch GetMessages")
|
||||
|
||||
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
|
||||
WorkManager.getInstance(this).enqueue(workRequest)
|
||||
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
|
||||
this, Observer { workInfo ->
|
||||
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
|
||||
val res = workInfo.outputData.keyValueMap.toString()
|
||||
result.success(workInfo.outputData.getString("result"))
|
||||
val profile = argmap["ProfileOnion"] ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val indexI: Int = call.argument("index") ?: 0
|
||||
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
|
||||
}
|
||||
|
||||
"GetSharedFiles" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
result.success(Cwtch.getSharedFiles(profile, conversation.toLong()))
|
||||
return
|
||||
}
|
||||
|
||||
"RestartSharing" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val filepath: String = call.argument("filekey") ?: ""
|
||||
result.success(Cwtch.restartSharing(profile, filepath))
|
||||
return
|
||||
}
|
||||
|
||||
"StopSharing" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val filepath: String = call.argument("filekey") ?: ""
|
||||
result.success(Cwtch.stopSharing(profile, filepath))
|
||||
return
|
||||
}
|
||||
|
||||
"CreateProfile" -> {
|
||||
val nick: String = call.argument("nick") ?: ""
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
Cwtch.createProfile(nick, pass)
|
||||
}
|
||||
"LoadProfiles" -> {
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
Cwtch.loadProfiles(pass)
|
||||
}
|
||||
"ChangePassword" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val pass: String = call.argument("OldPass") ?: ""
|
||||
val passNew: String = call.argument("NewPass") ?: ""
|
||||
val passNew2: String = call.argument("NewPassAgain") ?: ""
|
||||
Cwtch.changePassword(profile, pass, passNew, passNew2)
|
||||
}
|
||||
"GetMessage" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val indexI: Int = call.argument("index") ?: 0
|
||||
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(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(Cwtch.getMessagesByContentHash(profile, conversation.toLong(), contentHash))
|
||||
return
|
||||
}
|
||||
"SetMessageAttribute" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val channel: Int = call.argument("Chanenl") ?: 0
|
||||
val midx: Int = call.argument("Message") ?: 0
|
||||
val key: String = call.argument("key") ?: ""
|
||||
val value: String = call.argument("value") ?: ""
|
||||
Cwtch.setMessageAttribute(profile, conversation.toLong(), channel.toLong(), midx.toLong(), key, value)
|
||||
}
|
||||
"AcceptConversation" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.acceptConversation(profile, conversation.toLong())
|
||||
}
|
||||
"BlockContact" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.blockContact(profile, conversation.toLong())
|
||||
}
|
||||
"UnblockContact" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.unblockContact(profile, conversation.toLong())
|
||||
}
|
||||
|
||||
"DownloadFile" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val filepath: String = call.argument("filepath") ?: ""
|
||||
val manifestpath: String = call.argument("manifestpath") ?: ""
|
||||
val filekey: String = call.argument("filekey") ?: ""
|
||||
// FIXME: Prevent spurious calls by Intent
|
||||
if (profile != "") {
|
||||
Cwtch.downloadFile(profile, conversation.toLong(), filepath, manifestpath, filekey)
|
||||
}
|
||||
}
|
||||
)
|
||||
"CheckDownloadStatus" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val fileKey: String = call.argument("fileKey") ?: ""
|
||||
Cwtch.checkDownloadStatus(profile, fileKey)
|
||||
}
|
||||
"VerifyOrResumeDownload" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val fileKey: String = call.argument("fileKey") ?: ""
|
||||
Cwtch.verifyOrResumeDownload(profile, conversation.toLong(), fileKey)
|
||||
}
|
||||
"SendProfileEvent" -> {
|
||||
val onion: String= call.argument("onion") ?: ""
|
||||
val jsonEvent: String = call.argument("jsonEvent") ?: ""
|
||||
Cwtch.sendProfileEvent(onion, jsonEvent)
|
||||
}
|
||||
"SendAppEvent" -> {
|
||||
val jsonEvent: String = call.argument("jsonEvent") ?: ""
|
||||
Cwtch.sendAppEvent(jsonEvent)
|
||||
}
|
||||
"ResetTor" -> {
|
||||
Cwtch.resetTor()
|
||||
}
|
||||
"ImportBundle" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val bundle: String = call.argument("bundle") ?: ""
|
||||
result.success(Cwtch.importBundle(profile, bundle))
|
||||
}
|
||||
"CreateGroup" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val server: String = call.argument("server") ?: ""
|
||||
val groupName: String = call.argument("groupName") ?: ""
|
||||
Cwtch.createGroup(profile, server, groupName)
|
||||
}
|
||||
"DeleteProfile" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
Cwtch.deleteProfile(profile, pass)
|
||||
}
|
||||
"ArchiveConversation" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.archiveConversation(profile, conversation.toLong())
|
||||
}
|
||||
"DeleteConversation" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
Cwtch.deleteContact(profile, conversation.toLong())
|
||||
}
|
||||
"SetProfileAttribute" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val key: String = call.argument("Key") ?: ""
|
||||
val v: String = call.argument("Val") ?: ""
|
||||
Cwtch.setProfileAttribute(profile, key, v)
|
||||
}
|
||||
"SetConversationAttribute" -> {
|
||||
val profile: String = call.argument("ProfileOnion") ?: ""
|
||||
val conversation: Int = call.argument("conversation") ?: 0
|
||||
val key: String = call.argument("Key") ?: ""
|
||||
val v: String = call.argument("Val") ?: ""
|
||||
Cwtch.setConversationAttribute(profile, conversation.toLong(), key, v)
|
||||
}
|
||||
"LoadServers" -> {
|
||||
val password: String = call.argument("Password") ?: ""
|
||||
Cwtch.loadServers(password)
|
||||
}
|
||||
"CreateServer" -> {
|
||||
val password: String = call.argument("Password") ?: ""
|
||||
val desc: String = call.argument("Description") ?: ""
|
||||
val autostart: Boolean = call.argument("Autostart") ?: false
|
||||
Cwtch.createServer(password, desc, autostart)
|
||||
}
|
||||
"DeleteServer" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
val password: String = call.argument("Password") ?: ""
|
||||
Cwtch.deleteServer(serverOnion, password)
|
||||
}
|
||||
"LaunchServers" -> {
|
||||
Cwtch.launchServers()
|
||||
}
|
||||
"LaunchServer" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
Cwtch.launchServer(serverOnion)
|
||||
}
|
||||
"StopServer" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
Cwtch.stopServer(serverOnion)
|
||||
}
|
||||
"StopServers" -> {
|
||||
Cwtch.stopServers()
|
||||
}
|
||||
"DestroyServers" -> {
|
||||
Cwtch.destroyServers()
|
||||
}
|
||||
"SetServerAttribute" -> {
|
||||
val serverOnion: String = call.argument("ServerOnion") ?: ""
|
||||
val key: String = call.argument("Key") ?: ""
|
||||
val v: String = call.argument("Val") ?: ""
|
||||
Cwtch.setServerAttribute(serverOnion, key, v)
|
||||
}
|
||||
"ExportProfile" -> {
|
||||
val profileOnion: String = call.argument("ProfileOnion") ?: ""
|
||||
val file: String = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(call.argument("file") ?: "").toString()
|
||||
Log.i("FlwtchWorker", "constructing exported file " + file);
|
||||
Cwtch.exportProfile(profileOnion,file)
|
||||
}
|
||||
"ImportProfile" -> {
|
||||
val file: String = call.argument("file") ?: ""
|
||||
val pass: String = call.argument("pass") ?: ""
|
||||
Data.Builder().putString("result", Cwtch.importProfile(file, pass)).build()
|
||||
}
|
||||
"ReconnectCwtchForeground" -> {
|
||||
Cwtch.reconnectCwtchForeground()
|
||||
}
|
||||
"Shutdown" -> {
|
||||
Cwtch.shutdownCwtch();
|
||||
}
|
||||
else -> {
|
||||
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
|
||||
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
|
||||
WorkManager.getInstance(this).enqueue(workRequest)
|
||||
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
|
||||
this, Observer { workInfo ->
|
||||
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
|
||||
val res = workInfo.outputData.keyValueMap.toString()
|
||||
result.success(workInfo.outputData.getString("result"))
|
||||
}
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver
|
||||
|
@ -232,19 +558,22 @@ class MainActivity: FlutterActivity() {
|
|||
Log.i("MainActivity.kt", "onResume")
|
||||
if (myReceiver == null) {
|
||||
Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder")
|
||||
val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
|
||||
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS")
|
||||
myReceiver = MyBroadcastReceiver(mc)
|
||||
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(myReceiver!!, filter)
|
||||
val bm = flutterEngine?.dartExecutor?.binaryMessenger;
|
||||
if (bm != null) {
|
||||
val mc = MethodChannel(bm, CWTCH_EVENTBUS)
|
||||
|
||||
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS")
|
||||
myReceiver = MyBroadcastReceiver(mc)
|
||||
LocalBroadcastManager.getInstance(applicationContext)
|
||||
.registerReceiver(myReceiver!!, filter)
|
||||
}
|
||||
}
|
||||
|
||||
// ReconnectCwtchForeground which will resync counters and settings...
|
||||
// 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() {
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
app:lottie_autoPlay="true"
|
||||
app:lottie_rawRes="@raw/cwtch_animated_logo_op"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_speed="1.00" />
|
||||
app:lottie_speed="1.00"
|
||||
app:lottie_enableMergePathsForKitKatAndAbove="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,5 +1,5 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.5.31'
|
||||
repositories {
|
||||
google()
|
||||
// jCenter() no longer exists... https://blog.gradle.org/jcenter-shutdown
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
#Mon Jun 20 10:33:21 PDT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
After Width: | Height: | Size: 381 KiB |
Binary file not shown.
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
After Width: | Height: | Size: 346 KiB |
Binary file not shown.
After Width: | Height: | Size: 177 KiB |
|
@ -0,0 +1,16 @@
|
|||
Cwtch (/kʊtʃ/ - a Welsh word roughly translating to “a hug that creates a safe place”) is a decentralized,
|
||||
privacy-preserving, multi-party messaging protocol that can be used to build metadata resistant applications.
|
||||
|
||||
- Decentralized and Open: There is no “Cwtch service” or “Cwtch network”. Participants in Cwtch
|
||||
can host their own safe spaces, or lend their infrastructure to others seeking a safe space.
|
||||
The Cwtch protocol is open, and anyone is free to build bots, services and user interfaces and
|
||||
integrate and interact with Cwtch.
|
||||
|
||||
- Privacy Preserving: All communication in Cwtch is end-to-end encrypted and takes place over Tor v3
|
||||
onion services.
|
||||
|
||||
- Metadata Resistant: Cwtch has been designed such that no information is exchanged or available to
|
||||
anyone without their explicit consent, including on-the-wire messages and protocol metadata.
|
||||
|
||||
For more information on how Cwtch works and a guide to metadata resistant communication please
|
||||
checkout the Cwtch Handbook: https://docs.cwtch.im/
|
|
@ -0,0 +1 @@
|
|||
Metadata resistant privacy platform designed to help you resist surveillance
|
|
@ -0,0 +1 @@
|
|||
Cwtch
|
|
@ -0,0 +1 @@
|
|||
https://cwtch.im/cwtch-explainer.mp4
|
|
@ -3,4 +3,6 @@
|
|||
VERSION=`cat LIBCWTCH-GO-MACOS.version`
|
||||
echo $VERSION
|
||||
|
||||
curl https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.dylib --output libCwtch.dylib
|
||||
curl https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.x64.dylib --output libCwtch.x64.dylib
|
||||
curl https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.arm64.dylib --output libCwtch.arm64.dylib
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
cd macos
|
||||
curl https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-macos-0.4.6.7.tar.gz --output tor.tar.gz
|
||||
curl https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-macos-0.4.7.8.tar.gz --output tor.tar.gz
|
||||
tar -xzf tor.tar.gz
|
||||
chmod a+x Tor/tor.real
|
||||
cd ..
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.9.zip -OutFile tor.zip
|
||||
Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.7.8.zip -OutFile tor.zip
|
||||
|
||||
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne 'bd99de56ef5ef9516410783ce48d52311093b26e718bf3d0a94efbd754d1cf2d12543f096139d9c289985349d26ee89b2308be5927fa1b410ff4f7f3129d6830' ) { Write-Error 'tor.zip sha512sum mismatch' }
|
||||
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '5b8f900a37f6e90d7a945b3903d769383c7478042cb43b2105d2374186e1a536f1a4758a2823d1d5be71d53a81dcfd8243293e04f82812d355983df322823cf4' ) { Write-Error 'tor.zip sha512sum mismatch' }
|
||||
|
||||
Expand-Archive -Path tor.zip -DestinationPath Tor
|
||||
|
|
10
fetch-tor.sh
10
fetch-tor.sh
|
@ -1,12 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.5.9-linux-x86_64 -O linux/tor
|
||||
chmod a+x linux/tor
|
||||
cd linux
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.7.8-linux-x86_64.tar.gz -O tor.tar.gz
|
||||
tar -xzf tor.tar.gz
|
||||
cd ..
|
||||
|
||||
mkdir -p android/app/src/main/jniLibs/arm64-v8a
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.6.9-arm64 -O android/app/src/main/jniLibs/arm64-v8a/libtor.so
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.7.10-arm64 -O android/app/src/main/jniLibs/arm64-v8a/libtor.so
|
||||
chmod a+x android/app/src/main/jniLibs/arm64-v8a/libtor.so
|
||||
|
||||
mkdir -p android/app/src/main/jniLibs/armeabi-v7a
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.6.9-arm7 -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so
|
||||
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.7.10-arm7 -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so
|
||||
chmod a+x android/app/src/main/jniLibs/armeabi-v7a/libtor.so
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:cwtch/widgets/passwordfield.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
void showPasswordDialog(BuildContext context, String title, String action, Function(String) onEntered) {
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
CwtchPasswordField passwordField = CwtchPasswordField(
|
||||
controller: passwordController,
|
||||
validator: (passsword) {
|
||||
return null;
|
||||
});
|
||||
|
||||
// set up the buttons
|
||||
Widget cancelButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // dismiss dialog
|
||||
},
|
||||
);
|
||||
Widget continueButton = ElevatedButton(
|
||||
child: Text(action),
|
||||
onPressed: () {
|
||||
onEntered(passwordController.value.text);
|
||||
});
|
||||
|
||||
// set up the AlertDialog
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text(title),
|
||||
content: passwordField,
|
||||
actions: [
|
||||
cancelButton,
|
||||
continueButton,
|
||||
],
|
||||
);
|
||||
|
||||
// show the dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
);
|
||||
}
|
|
@ -28,3 +28,31 @@ void showFilePicker(BuildContext ctx, int maxBytes, Function(File) onSuccess, Fu
|
|||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> showCreateFilePicker(BuildContext ctx) async {
|
||||
// only allow one file picker at a time
|
||||
// note: ideally we would destroy file picker when leaving a conversation
|
||||
// but we don't currently have that option.
|
||||
// we need to store AppState in a variable because ctx might be destroyed
|
||||
// while awaiting for pickFiles.
|
||||
var appstate = Provider.of<AppState>(ctx, listen: false);
|
||||
appstate.disableFilePicker = true;
|
||||
// currently lockParentWindow only works on Windows...
|
||||
String? result = await FilePicker.platform.saveFile(lockParentWindow: true);
|
||||
appstate.disableFilePicker = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<String?> showSelectDirectoryPicker(BuildContext ctx) async {
|
||||
// only allow one file picker at a time
|
||||
// note: ideally we would destroy file picker when leaving a conversation
|
||||
// but we don't currently have that option.
|
||||
// we need to store AppState in a variable because ctx might be destroyed
|
||||
// while awaiting for pickFiles.
|
||||
var appstate = Provider.of<AppState>(ctx, listen: false);
|
||||
appstate.disableFilePicker = true;
|
||||
// currently lockParentWindow only works on Windows...
|
||||
String? result = await FilePicker.platform.getDirectoryPath(lockParentWindow: true);
|
||||
appstate.disableFilePicker = false;
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
)),
|
||||
));
|
||||
});
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:flutter/src/services/text_input.dart';
|
||||
|
||||
// To handle profiles that are "unencrypted" (i.e don't require a password to open) we currently create a profile with a defacto, hardcoded password.
|
||||
// Details: https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
|
||||
const DefaultPassword = "be gay do crime";
|
||||
|
||||
const LastMessageSeenTimeKey = "profile.lastMessageSeenTime";
|
||||
|
||||
abstract class Cwtch {
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<void> Start();
|
||||
|
@ -19,6 +19,11 @@ abstract class Cwtch {
|
|||
// ignore: non_constant_identifier_names
|
||||
void ChangePassword(String profile, String pass, String newpass, String newpassAgain);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void ExportProfile(String profile, String file);
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<dynamic> ImportProfile(String file, String pass);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void ResetTor();
|
||||
|
||||
|
@ -45,12 +50,25 @@ abstract class Cwtch {
|
|||
Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendMessage(String profile, int handle, String message);
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendInvitation(String profile, int handle, int target);
|
||||
Future<dynamic> GetMessages(String profile, int handle, int index, int count);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void ShareFile(String profile, int handle, String filepath);
|
||||
Future<dynamic> SendMessage(String profile, int handle, String message);
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<dynamic> SendInvitation(String profile, int handle, int target);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<dynamic> ShareFile(String profile, int handle, String filepath);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<dynamic> GetSharedFiles(String profile, int handle);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void RestartSharing(String profile, String filekey);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void StopSharing(String profile, String filekey);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
|
||||
// android-only
|
||||
|
@ -73,7 +91,7 @@ abstract class Cwtch {
|
|||
void CreateGroup(String profile, String server, String groupName);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void ImportBundle(String profile, String bundle);
|
||||
Future<dynamic> ImportBundle(String profile, String bundle);
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetProfileAttribute(String profile, String key, String val);
|
||||
// ignore: non_constant_identifier_names
|
||||
|
@ -103,11 +121,13 @@ abstract class Cwtch {
|
|||
Future<void> Shutdown();
|
||||
|
||||
// non-ffi
|
||||
String defaultDownloadPath();
|
||||
String? defaultDownloadPath();
|
||||
|
||||
bool isL10nInit();
|
||||
|
||||
void l10nInit(String notificationSimple, String notificationConversationInfo);
|
||||
|
||||
void dispose();
|
||||
|
||||
Future<dynamic> GetDebugInfo();
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ import '../config.dart';
|
|||
import '../errorHandler.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
typedef SeenMessageCallback = Function(String, int, DateTime);
|
||||
|
||||
// Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin)
|
||||
// Takes Notifiers and triggers them on appropriate events
|
||||
class CwtchNotifier {
|
||||
|
@ -32,6 +34,8 @@ class CwtchNotifier {
|
|||
String? notificationSimple;
|
||||
String? notificationConversationInfo;
|
||||
|
||||
SeenMessageCallback? seenMessageCallback;
|
||||
|
||||
CwtchNotifier(
|
||||
ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) {
|
||||
profileCN = pcn;
|
||||
|
@ -48,6 +52,10 @@ class CwtchNotifier {
|
|||
this.notificationConversationInfo = notificationConversationInfo;
|
||||
}
|
||||
|
||||
void setMessageSeenCallback(SeenMessageCallback callback) {
|
||||
seenMessageCallback = callback;
|
||||
}
|
||||
|
||||
void handleMessage(String type, dynamic data) {
|
||||
//EnvironmentConfig.debugLog("NewEvent $type $data");
|
||||
switch (type) {
|
||||
|
@ -153,18 +161,21 @@ class CwtchNotifier {
|
|||
}
|
||||
break;
|
||||
case "NewMessageFromPeer":
|
||||
|
||||
var identifier = int.parse(data["ConversationID"]);
|
||||
var messageID = int.parse(data["Index"]);
|
||||
var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
|
||||
var senderHandle = data['RemotePeer'];
|
||||
var senderImage = data['picture'];
|
||||
var isAuto = data['Auto'] == "true";
|
||||
String? contenthash = data['ContentHash'];
|
||||
String contenthash = data['ContentHash'];
|
||||
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
|
||||
var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
|
||||
var notification = data["notification"];
|
||||
|
||||
if (selectedConversation && seenMessageCallback != null) {
|
||||
seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc());
|
||||
}
|
||||
|
||||
if (notification == "SimpleEvent") {
|
||||
notificationManager.notify(notificationSimple ?? "New Message", "", 0);
|
||||
} else if (notification == "ContactInfo") {
|
||||
|
@ -212,7 +223,7 @@ class CwtchNotifier {
|
|||
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
||||
var currentTotal = contact!.totalMessages;
|
||||
var isAuto = data['Auto'] == "true";
|
||||
String? contenthash = data['ContentHash'];
|
||||
String contenthash = data['ContentHash'];
|
||||
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
|
||||
var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
|
||||
var notification = data["notification"];
|
||||
|
@ -230,6 +241,9 @@ class CwtchNotifier {
|
|||
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
|
||||
// and `local now`.
|
||||
profileCN.getProfile(data["ProfileOnion"])?.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedProfile, selectedConversation);
|
||||
if (selectedConversation && seenMessageCallback != null) {
|
||||
seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc());
|
||||
}
|
||||
|
||||
if (notification == "SimpleEvent") {
|
||||
notificationManager.notify(notificationSimple ?? "New Message", "", 0);
|
||||
|
@ -295,6 +309,14 @@ class CwtchNotifier {
|
|||
case "UpdateServerInfo":
|
||||
profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
|
||||
break;
|
||||
case "TokenManagerInfo":
|
||||
List<dynamic> associatedGroups = jsonDecode(data["Data"]);
|
||||
int count = int.parse(data["ServerTokenCount"]);
|
||||
associatedGroups.forEach((identifier) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(int.parse(identifier.toString()))!.antispamTickets = count;
|
||||
});
|
||||
EnvironmentConfig.debugLog("update server token count for ${associatedGroups}, $count");
|
||||
break;
|
||||
case "NewGroup":
|
||||
String invite = data["GroupInvite"].toString();
|
||||
if (invite.startsWith("torv3")) {
|
||||
|
|
|
@ -52,19 +52,31 @@ typedef StringFn = void Function(Pointer<Utf8> dir, int);
|
|||
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
|
||||
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
typedef string_string_to_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
|
||||
typedef StringFromStringStringFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
typedef string_int_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Int32 handle);
|
||||
typedef VoidFromStringIntFn = void Function(Pointer<Utf8>, int, int);
|
||||
|
||||
typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
|
||||
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len);
|
||||
|
||||
typedef get_json_blob_from_string_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
|
||||
typedef GetJsonBlobFromStrIntStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int);
|
||||
|
||||
//func GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char {
|
||||
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
|
||||
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
|
||||
|
||||
typedef get_json_blob_from_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32);
|
||||
typedef GetJsonBlobFromStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int);
|
||||
|
||||
typedef get_json_blob_from_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32);
|
||||
typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int);
|
||||
|
||||
typedef get_json_blob_from_str_int_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32, Int32);
|
||||
typedef GetJsonBlobFromStrIntIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int, int);
|
||||
|
||||
typedef get_json_blob_from_str_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
|
||||
typedef GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function(
|
||||
Pointer<Utf8>,
|
||||
|
@ -93,6 +105,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 {
|
||||
|
@ -108,8 +123,11 @@ class CwtchFfi implements Cwtch {
|
|||
} else if (Platform.isLinux) {
|
||||
return "libCwtch.so";
|
||||
} else if (Platform.isMacOS) {
|
||||
print(dirname(Platform.script.path));
|
||||
return "libCwtch.dylib";
|
||||
if (Abi.current() == Abi.macosX64) {
|
||||
return "libCwtch.x64.dylib";
|
||||
} else {
|
||||
return "libCwtch.arm64.dylib";
|
||||
}
|
||||
} else {
|
||||
return UNSUPPORTED_OS;
|
||||
}
|
||||
|
@ -124,6 +142,7 @@ class CwtchFfi implements Cwtch {
|
|||
}
|
||||
library = DynamicLibrary.open(libraryPath);
|
||||
cwtchNotifier = _cwtchNotifier;
|
||||
cwtchNotifier.setMessageSeenCallback((String profile, int conversation, DateTime time) => {this.SetConversationAttribute(profile, conversation, LastMessageSeenTimeKey, time.toIso8601String())});
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
|
@ -134,20 +153,26 @@ class CwtchFfi implements Cwtch {
|
|||
String cwtchDir = "";
|
||||
if (Platform.isLinux) {
|
||||
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, ".cwtch");
|
||||
if (await File("linux/tor").exists()) {
|
||||
bundledTor = "linux/tor";
|
||||
} else if (await File("lib/tor").exists()) {
|
||||
bundledTor = "lib/tor";
|
||||
} else if (await File(path.join(home, ".local/lib/cwtch/tor")).exists()) {
|
||||
bundledTor = path.join(home, ".local/lib/cwtch/tor");
|
||||
} else if (await File("/usr/lib/cwtch/tor").exists()) {
|
||||
bundledTor = "/usr/lib/cwtch/tor";
|
||||
if (await File("linux/Tor/tor").exists()) {
|
||||
bundledTor = "linux/Tor/tor";
|
||||
} else if (await File("lib/Tor/tor").exists()) {
|
||||
bundledTor = "lib/Tor/tor";
|
||||
} else if (await File(path.join(home, ".local/lib/cwtch/Tor/tor")).exists()) {
|
||||
bundledTor = path.join(home, ".local/lib/cwtch/Tor/tor");
|
||||
} else if (await File("/usr/lib/cwtch/Tor/tor").exists()) {
|
||||
bundledTor = "/usr/lib/cwtch/Tor/tor";
|
||||
} else {
|
||||
bundledTor = "tor";
|
||||
}
|
||||
} 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()) {
|
||||
|
@ -196,8 +221,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);
|
||||
|
@ -297,6 +323,20 @@ class CwtchFfi implements Cwtch {
|
|||
return jsonMessage;
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<dynamic> GetMessages(String profile, int handle, int index, int count) async {
|
||||
var getMessagesC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_int_function>>("c_GetMessages");
|
||||
// ignore: non_constant_identifier_names
|
||||
final GetMessages = getMessagesC.asFunction<GetJsonBlobFromStrIntIntIntFn>();
|
||||
final utf8profile = profile.toNativeUtf8();
|
||||
Pointer<Utf8> jsonMessageBytes = GetMessages(utf8profile, utf8profile.length, handle, index, count);
|
||||
String jsonMessage = jsonMessageBytes.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
|
||||
malloc.free(utf8profile);
|
||||
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendProfileEvent(String onion, String json) {
|
||||
|
@ -356,39 +396,48 @@ class CwtchFfi implements Cwtch {
|
|||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendMessage(String profileOnion, int contactHandle, String message) {
|
||||
var sendMessage = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_SendMessage");
|
||||
Future<dynamic> SendMessage(String profileOnion, int contactHandle, String message) async {
|
||||
var sendMessage = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_SendMessage");
|
||||
// ignore: non_constant_identifier_names
|
||||
final SendMessage = sendMessage.asFunction<VoidFromStringIntStringFn>();
|
||||
final SendMessage = sendMessage.asFunction<GetJsonBlobFromStrIntStrFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u3 = message.toNativeUtf8();
|
||||
SendMessage(u1, u1.length, contactHandle, u3, u3.length);
|
||||
Pointer<Utf8> jsonMessageBytes = SendMessage(u1, u1.length, contactHandle, u3, u3.length);
|
||||
String jsonMessage = jsonMessageBytes.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
|
||||
malloc.free(u1);
|
||||
malloc.free(u3);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendInvitation(String profileOnion, int contactHandle, int target) {
|
||||
var sendInvitation = library.lookup<NativeFunction<void_from_string_int_int_function>>("c_SendInvitation");
|
||||
Future<dynamic> SendInvitation(String profileOnion, int contactHandle, int target) async {
|
||||
var sendInvitation = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_SendInvitation");
|
||||
// ignore: non_constant_identifier_names
|
||||
final SendInvitation = sendInvitation.asFunction<VoidFromStringIntIntFn>();
|
||||
final SendInvitation = sendInvitation.asFunction<GetJsonBlobFromStrIntIntFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
SendInvitation(u1, u1.length, contactHandle, target);
|
||||
Pointer<Utf8> jsonMessageBytes = SendInvitation(u1, u1.length, contactHandle, target);
|
||||
String jsonMessage = jsonMessageBytes.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
|
||||
malloc.free(u1);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ShareFile(String profileOnion, int contactHandle, String filepath) {
|
||||
var shareFile = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_ShareFile");
|
||||
Future<dynamic> ShareFile(String profileOnion, int contactHandle, String filepath) async {
|
||||
var shareFile = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_ShareFile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ShareFile = shareFile.asFunction<VoidFromStringIntStringFn>();
|
||||
final ShareFile = shareFile.asFunction<GetJsonBlobFromStrIntStrFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u3 = filepath.toNativeUtf8();
|
||||
ShareFile(u1, u1.length, contactHandle, u3, u3.length);
|
||||
Pointer<Utf8> jsonMessageBytes = ShareFile(u1, u1.length, contactHandle, u3, u3.length);
|
||||
String jsonMessage = jsonMessageBytes.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
|
||||
malloc.free(u1);
|
||||
malloc.free(u3);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -456,15 +505,18 @@ class CwtchFfi implements Cwtch {
|
|||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ImportBundle(String profileOnion, String bundle) {
|
||||
var importBundle = library.lookup<NativeFunction<string_string_to_void_function>>("c_ImportBundle");
|
||||
Future<dynamic> ImportBundle(String profileOnion, String bundle) async {
|
||||
var importBundle = library.lookup<NativeFunction<string_string_to_string_function>>("c_ImportBundle");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ImportBundle = importBundle.asFunction<VoidFromStringStringFn>();
|
||||
final ImportBundle = importBundle.asFunction<StringFromStringStringFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = bundle.toNativeUtf8();
|
||||
ImportBundle(u1, u1.length, u2, u2.length);
|
||||
Pointer<Utf8> responsePtr = ImportBundle(u1, u1.length, u2, u2.length);
|
||||
String response = responsePtr.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(responsePtr);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
return response;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -710,9 +762,13 @@ class CwtchFfi implements Cwtch {
|
|||
}
|
||||
|
||||
@override
|
||||
String defaultDownloadPath() {
|
||||
String? defaultDownloadPath() {
|
||||
Map<String, String> envVars = Platform.environment;
|
||||
return path.join(envVars[Platform.isWindows ? 'UserProfile' : 'HOME']!, "Downloads");
|
||||
String nominalPath = path.join(envVars[Platform.isWindows ? 'UserProfile' : 'HOME']!, "Downloads");
|
||||
if (Directory(nominalPath).existsSync() == false) {
|
||||
return null;
|
||||
}
|
||||
return nominalPath;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -756,4 +812,79 @@ class CwtchFfi implements Cwtch {
|
|||
cwtchNotifier.l10nInit(notificationSimple, notificationConversationInfo);
|
||||
_isL10nInit = true;
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ExportProfile(String profile, String file) {
|
||||
final utf8profile = profile.toNativeUtf8();
|
||||
final utf8file = file.toNativeUtf8();
|
||||
var exportProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_ExportProfile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ExportProfileFn = exportProfileC.asFunction<VoidFromStringStringFn>();
|
||||
ExportProfileFn(utf8profile, utf8profile.length, utf8file, utf8file.length);
|
||||
malloc.free(utf8profile);
|
||||
malloc.free(utf8file);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<String> ImportProfile(String file, String pass) async {
|
||||
final utf8pass = pass.toNativeUtf8();
|
||||
final utf8file = file.toNativeUtf8();
|
||||
var exportProfileC = library.lookup<NativeFunction<string_string_to_string_function>>("c_ImportProfile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ExportProfileFn = exportProfileC.asFunction<StringFromStringStringFn>();
|
||||
Pointer<Utf8> result = ExportProfileFn(utf8file, utf8file.length, utf8pass, utf8pass.length);
|
||||
String importResult = result.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
|
||||
malloc.free(utf8pass);
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> GetSharedFiles(String profile, int handle) async {
|
||||
var getSharedFiles = library.lookup<NativeFunction<get_json_blob_from_str_int_function>>("c_GetSharedFiles");
|
||||
final GetSharedFiles = getSharedFiles.asFunction<GetJsonBlobFromStrIntFn>();
|
||||
final utf8profile = profile.toNativeUtf8();
|
||||
Pointer<Utf8> jsonMessageBytes = GetSharedFiles(utf8profile, utf8profile.length, handle);
|
||||
String jsonMessage = jsonMessageBytes.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
|
||||
malloc.free(utf8profile);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
void RestartSharing(String profile, String filekey) {
|
||||
var restartSharingC = library.lookup<NativeFunction<void_from_string_string_function>>("c_RestartSharing");
|
||||
// ignore: non_constant_identifier_names
|
||||
final RestartSharing = restartSharingC.asFunction<VoidFromStringStringFn>();
|
||||
final utf8profile = profile.toNativeUtf8();
|
||||
final ut8filekey = filekey.toNativeUtf8();
|
||||
RestartSharing(utf8profile, utf8profile.length, ut8filekey, ut8filekey.length);
|
||||
malloc.free(utf8profile);
|
||||
malloc.free(ut8filekey);
|
||||
}
|
||||
|
||||
@override
|
||||
void StopSharing(String profile, String filekey) {
|
||||
var stopSharingC = library.lookup<NativeFunction<void_from_string_string_function>>("c_StopSharing");
|
||||
// ignore: non_constant_identifier_names
|
||||
final StopSharing = stopSharingC.asFunction<VoidFromStringStringFn>();
|
||||
final utf8profile = profile.toNativeUtf8();
|
||||
final ut8filekey = filekey.toNativeUtf8();
|
||||
StopSharing(utf8profile, utf8profile.length, ut8filekey, ut8filekey.length);
|
||||
malloc.free(utf8profile);
|
||||
malloc.free(ut8filekey);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ class CwtchGomobile implements Cwtch {
|
|||
CwtchGomobile(CwtchNotifier _cwtchNotifier) {
|
||||
print("gomobile.dart: CwtchGomobile()");
|
||||
cwtchNotifier = _cwtchNotifier;
|
||||
cwtchNotifier.setMessageSeenCallback((String profile, int conversation, DateTime time) => {this.SetConversationAttribute(profile, conversation, LastMessageSeenTimeKey, time.toIso8601String())});
|
||||
androidHomeDirectory = getApplicationDocumentsDirectory();
|
||||
androidLibraryDir = appInfoPlatform.invokeMethod('getNativeLibDir');
|
||||
|
||||
|
@ -94,6 +95,11 @@ class CwtchGomobile implements Cwtch {
|
|||
return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id});
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<dynamic> GetMessages(String profile, int conversation, int index, int count) {
|
||||
return cwtchPlatform.invokeMethod("GetMessages", {"ProfileOnion": profile, "conversation": conversation, "index": index, "count": count});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendProfileEvent(String onion, String jsonEvent) {
|
||||
|
@ -129,20 +135,20 @@ class CwtchGomobile implements Cwtch {
|
|||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendMessage(String profileOnion, int conversation, String message) {
|
||||
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
|
||||
Future<dynamic> SendMessage(String profileOnion, int conversation, String message) {
|
||||
return cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendInvitation(String profileOnion, int conversation, int target) {
|
||||
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
|
||||
Future<dynamic> SendInvitation(String profileOnion, int conversation, int target) {
|
||||
return cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ShareFile(String profileOnion, int conversation, String filepath) {
|
||||
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
|
||||
Future<dynamic> ShareFile(String profileOnion, int conversation, String filepath) {
|
||||
return cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -184,8 +190,8 @@ class CwtchGomobile implements Cwtch {
|
|||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ImportBundle(String profileOnion, String bundle) {
|
||||
cwtchPlatform.invokeMethod("ImportBundle", {"ProfileOnion": profileOnion, "bundle": bundle});
|
||||
Future<dynamic> ImportBundle(String profileOnion, String bundle) {
|
||||
return cwtchPlatform.invokeMethod("ImportBundle", {"ProfileOnion": profileOnion, "bundle": bundle});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -288,7 +294,7 @@ class CwtchGomobile implements Cwtch {
|
|||
}
|
||||
|
||||
@override
|
||||
String defaultDownloadPath() {
|
||||
String? defaultDownloadPath() {
|
||||
return this.androidHomeDirectoryStr;
|
||||
}
|
||||
|
||||
|
@ -308,4 +314,38 @@ class CwtchGomobile implements Cwtch {
|
|||
cwtchPlatform.invokeMethod("L10nInit", {"notificationSimple": notificationSimple, "notificationConversationInfo": notificationConversationInfo});
|
||||
_isL10nInit = true;
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ExportProfile(String profile, String file) {
|
||||
cwtchPlatform.invokeMethod("ExportProfile", {"ProfileOnion": profile, "file": file});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
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("{}");
|
||||
}
|
||||
|
||||
@override
|
||||
Future GetSharedFiles(String profile, int handle) {
|
||||
return cwtchPlatform.invokeMethod("GetSharedFiles", {"ProfileOnion": profile, "conversation": handle});
|
||||
}
|
||||
|
||||
@override
|
||||
void RestartSharing(String profile, String filekey) {
|
||||
cwtchPlatform.invokeMethod("RestartSharing", {"ProfileOnion": profile, "filekey": filekey});
|
||||
}
|
||||
|
||||
@override
|
||||
void StopSharing(String profile, String filekey) {
|
||||
cwtchPlatform.invokeMethod("StopSharing", {"ProfileOnion": profile, "filekey": filekey});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/// Flutter icons CwtchIcons
|
||||
/// Copyright (C) 2021 by Open Privacy Research Society via fluttericon.com, fontello.com
|
||||
/// Flutter icons Cwtch
|
||||
/// Copyright (C) 2021-2022 by Open Privacy Research Society
|
||||
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
|
||||
///
|
||||
/// To use this font, place it in your fonts/ directory and include the
|
||||
|
@ -7,9 +7,9 @@
|
|||
///
|
||||
/// flutter:
|
||||
/// fonts:
|
||||
/// - family: CwtchIcons
|
||||
/// - family: Cwtch
|
||||
/// fonts:
|
||||
/// - asset: assets/fonts/CwtchIcons.ttf
|
||||
/// - asset: fonts/Cwtch.ttf
|
||||
///
|
||||
///
|
||||
///
|
||||
|
@ -18,103 +18,112 @@ import 'package:flutter/widgets.dart';
|
|||
class CwtchIcons {
|
||||
CwtchIcons._();
|
||||
|
||||
static const _kFontFam = 'CwtchIcons';
|
||||
static const _kFontFam = 'Cwtch';
|
||||
static const String? _kFontPkg = null;
|
||||
|
||||
static const IconData arrow_back_24px = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData attach_file_24px = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData block_peer = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData block_unknown = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData block_24px = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData brightness_5_24px = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData camera_alt_24px = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData change_language = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData change_theme = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chat_bubble_empty_24px = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chat_bubble_24px = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chat_seetings_24px = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData check_24px = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chevron_left_24px = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData clear_24px = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData content_copy_24px = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData create_group = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData cwtch_knott = IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dark_mode_24px = IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData delete_24px = IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_24px = IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData drag_indicator_24px = IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData edit_24px = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData enable_experiments = IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData enable_groups = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_closed = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_open = IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_24dp = IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_broken = IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_brokenhalf = IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_malformed = IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_sad = IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData group_add_24px = IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData group_settings_24px = IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData groups_24px = IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData info_24px = IconData(0xe823, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData join_group = IconData(0xe824, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData list_black_24dp = IconData(0xe825, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData lock_open_24px = IconData(0xe826, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData lock_24px = IconData(0xe827, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData maps_ugc_24px = IconData(0xe828, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData menu_24px = IconData(0xe829, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData message_24px = IconData(0xe82a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData mood_24px = IconData(0xe82b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData more_vert_24px = IconData(0xe82c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData negative_heart_24px = IconData(0xe82d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData onion_off = IconData(0xe82e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData onion_on = IconData(0xe82f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData onion_waiting = IconData(0xe830, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData peer_history = IconData(0xe831, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData peer_settings_24px = IconData(0xe832, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData person_add_alt_1_24px = IconData(0xe833, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData person_add_24px = IconData(0xe834, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData person_24px = IconData(0xe835, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData push_pin_black_24dp = IconData(0xe836, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData push_pin_24px = IconData(0xe837, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData search_24px = IconData(0xe838, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData send_24px = IconData(0xe839, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData settings_24px = IconData(0xe83a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_4_bar_24px = IconData(0xe83b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_alt_24px = IconData(0xe83c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_connected_no_internet_4_bar_24px = IconData(0xe83d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_off_24px = IconData(0xe83e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData swap_horiz_24px = IconData(0xe83f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sync_disabled_24px = IconData(0xe840, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sync_problem_24px = IconData(0xe841, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sync_24px = IconData(0xe842, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData syncing_01 = IconData(0xe843, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData syncing_02 = IconData(0xe844, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData syncing_03 = IconData(0xe845, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData toggle_on_24px = IconData(0xe846, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData vpn_key_24px = IconData(0xe847, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_blocked = IconData(0xe848, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px = IconData(0xe849, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px_lines = IconData(0xe84a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px_lines_thin___blocked = IconData(0xe84b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px_user = IconData(0xe84c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_circle_24px = IconData(0xe84d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_group = IconData(0xe84e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_peer = IconData(0xe84f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_24px = IconData(0xe850, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData address_copy_2 = IconData(0xe852, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData address = IconData(0xe856, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData streamer_bunnymask = IconData(0xe85b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData streamer_ghost = IconData(0xe85c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData cancel_schedule_send_black_24dp = IconData(0xe85d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData more_horiz_black_24dp = IconData(0xe85e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_black_add_24dp = IconData(0xe85f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_black_24dp = IconData(0xe860, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData info_black_24dp = IconData(0xe861, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData accept_unknown = IconData(0xe862, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_closed_1 = IconData(0xe863, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_open_1 = IconData(0xe864, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData send_invite = IconData(0xe888, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData leave_group = IconData(0xe88a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData leave_chat = IconData(0xe88b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_blocked = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px_lines = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px_lines_thin___blocked = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData account_circle_24px_user = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_circle_24px = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_group = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_peer = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData add_24px = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData address = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData address_copy = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData alternate_email_black_24dp = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData arrow_back_24px = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData attach_file_24px = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData attached_file = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData block_peer = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData block_unknown = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData block_24px = IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData brightness_5_24px = IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData camera_alt_24px = IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData cancel_schedule_send_black_24dp = IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData change_language = IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData change_theme = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chat_bubble_empty_24px = IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chat_bubble_24px = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chat_seetings_24px = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData check_24px = IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData chevron_left_24px = IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData clear_24px = IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData content_copy_24px = IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData copy_address = IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData create_group = IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData cwtch_knott = IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dark_mode_24px = IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData delete_24px = IconData(0xe823, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_black_24dp = IconData(0xe824, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_black_add_24dp = IconData(0xe825, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_24px = IconData(0xe826, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData drag_indicator_24px = IconData(0xe827, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData edit_24px = IconData(0xe828, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData enable_experiments = IconData(0xe829, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData enable_groups = IconData(0xe82a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_closed = IconData(0xe82b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_open = IconData(0xe82c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_24dp = IconData(0xe82d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_broken = IconData(0xe82e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_brokenhalf = IconData(0xe82f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_malformed = IconData(0xe830, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_black_24dp_sad = IconData(0xe831, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData folder_black_24dp = IconData(0xe832, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData group_add_24px = IconData(0xe833, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData group_settings_24px = IconData(0xe834, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData groups_24px = IconData(0xe835, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData image_black_24dp = IconData(0xe836, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData info_black_24dp = IconData(0xe837, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData info_24px = IconData(0xe838, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData join_group = IconData(0xe839, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData leave_chat = IconData(0xe83a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData leave_group = IconData(0xe83b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData link_ = IconData(0xe83c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData list_black_24dp = IconData(0xe83d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData lock_open_24px = IconData(0xe83e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData lock_24px = IconData(0xe83f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData maps_ugc_24px = IconData(0xe841, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData menu_24px = IconData(0xe842, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData message_24px = IconData(0xe843, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData mood_24px = IconData(0xe844, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData more_horiz_black_24dp = IconData(0xe845, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData more_vert_24px = IconData(0xe846, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData negative_heart_24px = IconData(0xe848, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData onion_off = IconData(0xe849, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData onion_on = IconData(0xe84a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData onion_waiting = IconData(0xe84b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData peer_history = IconData(0xe84c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData peer_settings_24px = IconData(0xe84d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData person_add_alt_1_24px = IconData(0xe84e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData person_add_24px = IconData(0xe84f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData person_24px = IconData(0xe850, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData push_pin_black_24dp = IconData(0xe852, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData push_pin_24px = IconData(0xe853, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData search_24px = IconData(0xe855, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData send_invite = IconData(0xe856, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData send_24px = IconData(0xe857, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData settings_24px = IconData(0xe858, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_4_bar_24px = IconData(0xe859, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_alt_24px = IconData(0xe85a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_connected_no_internet_4_bar_24px = IconData(0xe85b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData signal_cellular_off_24px = IconData(0xe85c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData streamer_bunnymask = IconData(0xe85d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData streamer_ghost = IconData(0xe85e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData swap_horiz_24px = IconData(0xe85f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sync_disabled_24px = IconData(0xe860, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sync_problem_24px = IconData(0xe861, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData sync_24px = IconData(0xe862, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData syncing_01 = IconData(0xe863, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData syncing_02 = IconData(0xe864, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData syncing_03 = IconData(0xe865, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData toggle_on_24px = IconData(0xe866, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData vpn_key_24px = IconData(0xe867, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData accept_unknown = IconData(0xe868, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData view_replies = IconData(0xe869, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData anti_spam_2 = IconData(0xe86b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData manage_files = IconData(0xe86c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData attached_file_2 = IconData(0xe86d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData anti_spam_3 = IconData(0xe86e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
}
|
||||
|
|
|
@ -5,23 +5,14 @@ class ErrorHandler extends ChangeNotifier {
|
|||
static const String successErrorType = "success";
|
||||
|
||||
// Add Contact Specific Errors...
|
||||
static const String addContactErrorPrefix = "addcontact";
|
||||
static const String changePasswordErrorPrefix = "changepassword";
|
||||
static const String invalidImportStringErrorType = "invalid_import_string";
|
||||
static const String contactAlreadyExistsErrorType = "contact_already_exists";
|
||||
bool invalidImportStringError = false;
|
||||
bool contactAlreadyExistsError = false;
|
||||
bool explicitAddContactSuccess = false;
|
||||
|
||||
// ChangePassword
|
||||
bool changePasswordError = false;
|
||||
bool explicitChangePasswordSuccess = false;
|
||||
|
||||
// Import Bundle Specific Errors
|
||||
static const String importBundleErrorPrefix = "importBundle";
|
||||
bool importBundleError = false;
|
||||
bool importBundleSuccess = false;
|
||||
|
||||
static const String deleteProfileErrorPrefix = "deleteprofile";
|
||||
bool deleteProfileError = false;
|
||||
bool deleteProfileSuccess = false;
|
||||
|
@ -31,13 +22,6 @@ class ErrorHandler extends ChangeNotifier {
|
|||
bool deletedServerSuccess = false;
|
||||
|
||||
reset() {
|
||||
invalidImportStringError = false;
|
||||
contactAlreadyExistsError = false;
|
||||
explicitAddContactSuccess = false;
|
||||
|
||||
importBundleError = false;
|
||||
importBundleSuccess = false;
|
||||
|
||||
deleteProfileError = false;
|
||||
deleteProfileSuccess = false;
|
||||
|
||||
|
@ -57,12 +41,6 @@ class ErrorHandler extends ChangeNotifier {
|
|||
String errorType = parts[1];
|
||||
|
||||
switch (prefix) {
|
||||
case addContactErrorPrefix:
|
||||
handleAddContactError(errorType);
|
||||
break;
|
||||
case importBundleErrorPrefix:
|
||||
handleImportBundleError(errorType);
|
||||
break;
|
||||
case deleteProfileErrorPrefix:
|
||||
handleDeleteProfileError(errorType);
|
||||
break;
|
||||
|
@ -76,41 +54,6 @@ class ErrorHandler extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
handleAddContactError(String errorType) {
|
||||
// Reset add contact errors
|
||||
invalidImportStringError = false;
|
||||
contactAlreadyExistsError = false;
|
||||
explicitAddContactSuccess = false;
|
||||
|
||||
switch (errorType) {
|
||||
case invalidImportStringErrorType:
|
||||
invalidImportStringError = true;
|
||||
break;
|
||||
case contactAlreadyExistsErrorType:
|
||||
contactAlreadyExistsError = true;
|
||||
break;
|
||||
case successErrorType:
|
||||
explicitAddContactSuccess = true;
|
||||
importBundleSuccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleImportBundleError(String errorType) {
|
||||
// Reset add contact errors
|
||||
importBundleError = false;
|
||||
importBundleSuccess = false;
|
||||
|
||||
switch (errorType) {
|
||||
case successErrorType:
|
||||
importBundleSuccess = true;
|
||||
break;
|
||||
default:
|
||||
importBundleError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteProfileError(String errorType) {
|
||||
// Reset add contact errors
|
||||
deleteProfileError = false;
|
||||
|
|
|
@ -312,6 +312,182 @@ class MaterialLocalizationLu extends MaterialLocalizations {
|
|||
@override
|
||||
String get viewLicensesButtonLabel => 'LIZENZEN ANZEIGEN';
|
||||
|
||||
// ***** NEW *****
|
||||
|
||||
@override
|
||||
String get keyboardKeyAlt => 'Alt';
|
||||
|
||||
@override
|
||||
String get keyboardKeyAltGraph => 'AltGr';
|
||||
|
||||
@override
|
||||
String get keyboardKeyBackspace => 'Backspace';
|
||||
|
||||
@override
|
||||
String get keyboardKeyCapsLock => 'Caps Lock';
|
||||
|
||||
@override
|
||||
String get keyboardKeyChannelDown => 'Kanal Erof';
|
||||
|
||||
@override
|
||||
String get keyboardKeyChannelUp => 'Kanal Up';
|
||||
|
||||
@override
|
||||
String get keyboardKeyControl => 'Ctrl';
|
||||
|
||||
@override
|
||||
String get keyboardKeyDelete => 'Del';
|
||||
|
||||
@override
|
||||
String get keyboardKeyEisu => 'Eisū';
|
||||
|
||||
@override
|
||||
String get keyboardKeyEject => 'Eject';
|
||||
|
||||
@override
|
||||
String get keyboardKeyEnd => 'End';
|
||||
|
||||
@override
|
||||
String get keyboardKeyEscape => 'Esc';
|
||||
|
||||
@override
|
||||
String get keyboardKeyFn => 'Fn';
|
||||
|
||||
@override
|
||||
String get keyboardKeyHangulMode => 'Hangul Mode';
|
||||
|
||||
@override
|
||||
String get keyboardKeyHanjaMode => 'Hanja Mode';
|
||||
|
||||
@override
|
||||
String get keyboardKeyHankaku => 'Hankaku';
|
||||
|
||||
@override
|
||||
String get keyboardKeyHiragana => 'Hiragana';
|
||||
|
||||
@override
|
||||
String get keyboardKeyHiraganaKatakana => 'Hiragana Katakana';
|
||||
|
||||
@override
|
||||
String get keyboardKeyHome => 'Home';
|
||||
|
||||
@override
|
||||
String get keyboardKeyInsert => 'Insert';
|
||||
|
||||
@override
|
||||
String get keyboardKeyKanaMode => 'Kana Mode';
|
||||
|
||||
@override
|
||||
String get keyboardKeyKanjiMode => 'Kanji Mode';
|
||||
|
||||
@override
|
||||
String get keyboardKeyKatakana => 'Katakana';
|
||||
|
||||
@override
|
||||
String get keyboardKeyMeta => 'Meta';
|
||||
|
||||
@override
|
||||
String get keyboardKeyMetaMacOs => 'Command';
|
||||
|
||||
@override
|
||||
String get keyboardKeyMetaWindows => 'Win';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumLock => 'Num Lock';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad0 => 'Num 0';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad1 => 'Num 1';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad2 => 'Num 2';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad3 => 'Num 3';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad4 => 'Num 4';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad5 => 'Num 5';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad6 => 'Num 6';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad7 => 'Num 7';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad8 => 'Num 8';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpad9 => 'Num 9';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadAdd => 'Num +';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadComma => 'Num ,';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadDecimal => 'Num .';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadDivide => 'Num /';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadEnter => 'Num Enter';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadEqual => 'Num =';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadMultiply => 'Num *';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadParenLeft => 'Num (';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadParenRight => 'Num )';
|
||||
|
||||
@override
|
||||
String get keyboardKeyNumpadSubtract => 'Num -';
|
||||
|
||||
@override
|
||||
String get keyboardKeyPageDown => 'PgDown';
|
||||
|
||||
@override
|
||||
String get keyboardKeyPageUp => 'PgUp';
|
||||
|
||||
@override
|
||||
String get keyboardKeyPower => 'Power';
|
||||
|
||||
@override
|
||||
String get keyboardKeyPowerOff => 'Power Off';
|
||||
|
||||
@override
|
||||
String get keyboardKeyPrintScreen => 'Print Screen';
|
||||
|
||||
@override
|
||||
String get keyboardKeyRomaji => 'Romaji';
|
||||
|
||||
@override
|
||||
String get keyboardKeyScrollLock => 'Scroll Lock';
|
||||
|
||||
@override
|
||||
String get keyboardKeySelect => 'Select';
|
||||
|
||||
@override
|
||||
String get keyboardKeySpace => 'Spasie';
|
||||
|
||||
@override
|
||||
String get keyboardKeyZenkaku => 'Zenkaku';
|
||||
|
||||
@override
|
||||
String get keyboardKeyZenkakuHankaku => 'Zenkaku Hankaku';
|
||||
|
||||
@override
|
||||
String aboutListTileTitle(String applicationName) {
|
||||
return aboutListTileTitleRaw.replaceFirst("$applicationName", applicationName);
|
||||
|
@ -442,4 +618,8 @@ class MaterialLocalizationLu extends MaterialLocalizations {
|
|||
// TODO: implement timeOfDayFormat
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement menuBarMenuLabel
|
||||
String get menuBarMenuLabel => throw UnimplementedError();
|
||||
}
|
||||
|
|
|
@ -1,9 +1,68 @@
|
|||
{
|
||||
"@@locale": "cy",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Tyrceg \/ Türk",
|
||||
"localeIt": "Eidaleg \/ Italiana",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Almaeneg \/ Deutsch",
|
||||
"localePt": "Portiwgaleg \/ Portuguesa",
|
||||
"localeRo": "Rwmaneg \/ Română",
|
||||
"localeEl": "Groeg \/ Ελληνικά",
|
||||
"localeLb": "Lwcsembwrg \/ Lëtzebuergesch",
|
||||
"localeRU": "Rwsieg \/ Русский",
|
||||
"localeCy": "Cymraeg \/ Cymraeg",
|
||||
"localePl": "Pwylaidd \/ Polski",
|
||||
"localeFr": "Ffrangeg \/ Français",
|
||||
"localeEs": "Sbaeneg \/ Español",
|
||||
"localeNo": "Norwyaidd \/ Norsk",
|
||||
"localeDa": "Daneg \/ Dansk",
|
||||
"localeEn": "Saesneg \/ English",
|
||||
"tooltipBoldText": "Beiddgar",
|
||||
"tooltipCode": "Cod \/ Monospace",
|
||||
"exportProfileTooltip": "Gwneud copi wrth gefn o'r proffil hwn i ffeil wedi'i hamgryptio. Gellir mewngludo'r ffeil wedi'i hamgryptio i ap Cwtch arall.",
|
||||
"settingAndroidPowerExemption": "Android Anwybyddu Optimeiddio Batri",
|
||||
"messageFormattingDescription": "Galluogi fformatio testun cyfoethog mewn negeseuon wedi'u harddangos e.e. **beiddgar** a *italig*",
|
||||
"tooltipBackToMessageEditing": "Yn ôl i Olygu Negeseuon",
|
||||
"clickableLinksWarning": "Bydd agor yr URL hwn yn lansio cais y tu allan i Cwtch a gall ddatgelu metadata neu beryglu diogelwch Cwtch fel arall. Dim ond agor URLs gan bobl rydych chi'n ymddiried ynddyn nhw. Ydych chi'n siŵr eich bod am barhau?",
|
||||
"settingsAndroidPowerReenablePopup": "Does dim modd ail-alluogi Optimeiddio Batri o fewn Cwtch. Ewch i Android \/ Gosodiadau \/ Apps \/ Cwtch \/ Batri a gosod Defnydd i 'Optimized'",
|
||||
"tooltipItalicize": "Italig",
|
||||
"tooltipSuperscript": "Uwchysgrif",
|
||||
"settingAndroidPowerExemptionDescription": "Dewisol: Gofynnwch i Android eithrio Cwtch rhag rheoli pŵer optimized. Bydd hyn yn arwain at well sefydlogrwydd ar draul mwy o ddefnydd o fatri.",
|
||||
"successfullyImportedProfile": "Proffil wedi'i Fewngludo'n Llwyddiannus: %profile",
|
||||
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Mae'r nodwedd hon yn gofyn i'r Arbrawf Grwpiau gael ei alluogi mewn Gosodiadau",
|
||||
"tooltipPreviewFormatting": "Rhagolwg Fformatio Negeseuon",
|
||||
"clickableLinkError": "Gwall wrth geisio agor URL",
|
||||
"importProfileTooltip": "Defnyddiwch gopi wrth gefn Cwtch wedi'i amgryptio i gyflwyno proffil a grëwyd mewn achos arall o Cwtch.",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Is-sgript",
|
||||
"settingImagePreviewsDescription": "Bydd delweddau a Lluniau Proffil yn cael eu lawrlwytho a'u rhagolwg yn awtomatig. Rydym yn argymell nad ydych yn galluogi'r Arbrawf hwn os ydych yn defnyddio Cwtch gyda chysylltiadau diymwad.",
|
||||
"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",
|
||||
"deleteConfirmLabel": "Teipiwch DILEU i gadarnhau",
|
||||
"deleteConfirmText": "DILEU",
|
||||
"localeDa": "Daneg",
|
||||
"successfullAddedContact": "Wedi llwyddo i ychwanegu: ",
|
||||
"serverMetricsLabel": "Metrigau Gweinydd",
|
||||
"manageKnownServersLong": "Rheoli Gweinyddwyr Hysbys",
|
||||
|
@ -25,7 +84,6 @@
|
|||
"torSettingsCustomControlPortDescription": "Defnyddiwch borthladd wedi'i deilwra ar gyfer cysylltiadau rheoli i'r dirprwy Tor",
|
||||
"torSettingsUseCustomTorServiceConfiguration": "Defnyddiwch Ffurfweddiad Gwasanaeth Custom Tor (torrc)",
|
||||
"fileSharingSettingsDownloadFolderDescription": "Pan fydd ffeiliau'n cael eu llwytho i lawr yn awtomatig (ee ffeiliau delwedd, pan fydd rhagolygon delwedd yn cael eu galluogi) mae angen lleoliad rhagosodedig i lawrlwytho'r ffeiliau iddo.",
|
||||
"settingImagePreviewsDescription": "Bydd delweddau'n cael eu lawrlwytho a'u rhagolwg yn awtomatig. Sylwch y gall rhagolygon delwedd yn aml arwain at wendidau diogelwch, ac ni ddylech alluogi'r Arbrawf hwn os ydych yn defnyddio Cwtch gyda chysylltiadau di-ymddiried. Mae lluniau proffil wedi'u cynllunio ar gyfer Cwtch 1.6.",
|
||||
"deleteServerConfirmBtn": "Mewn gwirionedd dileu gweinydd",
|
||||
"deleteServerSuccess": "Wedi dileu gweinydd yn llwyddiannus",
|
||||
"enterServerPassword": "Rhowch gyfrinair i ddatgloi gweinydd",
|
||||
|
@ -81,7 +139,6 @@
|
|||
"serverConnectionsLabel": "Cysylltiad",
|
||||
"torSettingsEnableCache": "Cache Consensws Tor",
|
||||
"notificationPolicyDefaultAll": "Diofyn Pawb",
|
||||
"localeCy": "Cymraeg",
|
||||
"tooltipRemoveThisQuotedMessage": "Dileu'r neges a ddyfynnwyd.",
|
||||
"settingUIColumnDouble14Ratio": "Dwbl (1:4)",
|
||||
"settingUIColumnDouble12Ratio": "Dwbl (1:2)",
|
||||
|
@ -136,7 +193,6 @@
|
|||
"themeNameCwtch": "Cwtch",
|
||||
"leaveConversation": "Gadael y Sgwrs Hon",
|
||||
"notificationNewMessageFromGroup": "Neges newydd mewn grŵp!",
|
||||
"localePl": "Pwylaidd",
|
||||
"plainServerDescription": "Rydym yn argymell eich bod yn diogelu eich proffiliau Cwtch gyda chyfrinair. Os nad ydych yn gosod cyfrinair ar y proffil hwn yna mae'n bosibl y bydd unrhyw un sydd â mynediad i'r ddyfais hon yn gallu cyrchu gwybodaeth am y proffil hwn, gan gynnwys cysylltiadau, negeseuon ac allweddi cryptograffig sensitif.",
|
||||
"encryptedServerDescription": "Mae amgryptio proffil gyda chyfrinair yn ei amddiffyn rhag pobl eraill a allai ddefnyddio'r ddyfais hon hefyd. Ni ellir dadgryptio, arddangos na chyrchu proffiliau wedi'u hamgryptio nes bod y cyfrinair cywir wedi'i fewnbynnu i'w datgloi.",
|
||||
"fieldDescriptionLabel": "Disgrifiad",
|
||||
|
@ -149,10 +205,6 @@
|
|||
"notificationContentContactInfo": "Gwybodaeth am Sgyrsiau",
|
||||
"newMessageNotificationSimple": "Neges Newydd",
|
||||
"newMessageNotificationConversationInfo": "Neges Newydd gan %1",
|
||||
"localeRo": "Romanaidd",
|
||||
"localeLb": "Lwcsembwrgaidd",
|
||||
"localeEl": "Groegaidd",
|
||||
"localeNo": "Norwyaidd",
|
||||
"retrievingManifestMessage": "Wrthi'n nôl gwybodaeth ffeil...",
|
||||
"streamerModeLabel": "Streamer Cyflwyno",
|
||||
"blockUnknownConnectionsEnabledDescription": "Mae cysylltiadau o gysylltiadau anhysbys wedi'u rhwystro. Gallwch newid hyn mewn Gosodiadau",
|
||||
|
@ -235,7 +287,6 @@
|
|||
"displayNameTooltip": "Rhowch enw arddangos",
|
||||
"importLocalServerButton": "Mewngludo %1",
|
||||
"newMessagesLabel": "Negeseuon Newydd",
|
||||
"localeRU": "Rwsaidd",
|
||||
"descriptionFileSharing": "Mae'r arbrawf rhannu ffeiliau yn caniatáu i chi anfon a derbyn ffeiliau o gysylltiadau a grwpiau Cwtch. Sylwer y bydd rhannu ffeil gyda grŵp yn arwain at aelodau o'r grŵp hwnnw yn cysylltu â chi'n uniongyrchol dros Cwtch i'w lawrlwytho.",
|
||||
"titleManageProfilesShort": "Proffiliau",
|
||||
"settingFileSharing": "Rhannu Ffeiliau",
|
||||
|
@ -265,8 +316,6 @@
|
|||
"accepted": "Derbyniwyd!",
|
||||
"newPassword": "Cyfrinair Newydd",
|
||||
"yesLeave": "Ydw, Gadael Y Sgwrs Hon",
|
||||
"localeIt": "Eidaleg",
|
||||
"localeEs": "Espanol",
|
||||
"builddate": "Adeiladwyd ar: % 2",
|
||||
"version": "Fersiwn %1",
|
||||
"versionTor": "Fersiwn %1 gyda rhwygo %2",
|
||||
|
@ -276,10 +325,6 @@
|
|||
"settingTheme": "Defnyddio Themâu Golau",
|
||||
"largeTextLabel": "Mawr",
|
||||
"settingInterfaceZoom": "Lefel Chwyddo",
|
||||
"localeDe": "Deutsche",
|
||||
"localePt": "Portuguesa",
|
||||
"localeFr": "Frances",
|
||||
"localeEn": "Saesneg",
|
||||
"settingLanguage": "Laith",
|
||||
"password": "Cyfrinair",
|
||||
"addNewProfileBtn": "Ychwanegu proffil newydd",
|
||||
|
|
|
@ -1,16 +1,70 @@
|
|||
{
|
||||
"@@locale": "da",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Tyrkisk \/ Türk",
|
||||
"localeIt": "Italiensk \/ Italiano",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Tysk \/ Deutsch",
|
||||
"localeEn": "Engelsk \/ English",
|
||||
"localeFr": "Fransk \/ Français",
|
||||
"localePt": "Portugisisk \/ Portuguesa",
|
||||
"localeEs": "Spansk \/ Español",
|
||||
"localePl": "Polsk \/ Polski",
|
||||
"localeRU": "Russisk \/ Русский",
|
||||
"localeDa": "Danish \/ Dansk",
|
||||
"localeCy": "Walisisk \/ Cymraeg",
|
||||
"localeEl": "Greek \/ Ελληνικά",
|
||||
"localeNo": "Norsk \/ Norsk",
|
||||
"localeLb": "Luxembourgske \/ Lëtzebuergesch",
|
||||
"localeRo": "Rumænsk \/ Română",
|
||||
"settingImagePreviewsDescription": "Billeder vil automatisk blive hentet og vist. Vær opmærksom på at visning af billeder ofte fører til sikkerhedsmæssige sårbarheder og at du ikke bør aktivere dette eksperiment hvis du bruger Cwtch med kontakter du ikke har tillid til. Profilbilleder er planlagt til Cwtch 1.6.",
|
||||
"tooltipPreviewFormatting": "Preview Message Formatting",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipItalicize": "Italic",
|
||||
"tooltipBackToMessageEditing": "Back to Message Editing",
|
||||
"tooltipBoldText": "Bold",
|
||||
"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",
|
||||
"serverLabel": "Server",
|
||||
"profileOnionLabel": "Send denne adresse til personer du ønsker forbindelse med",
|
||||
"saveBtn": "Gem",
|
||||
"deleteBtn": "Slet",
|
||||
"localeCy": "Walisisk",
|
||||
"localeDa": "Dansk",
|
||||
"localeEl": "Græsk",
|
||||
"localeNo": "Norsk",
|
||||
"localeLb": "Luxembourgisk",
|
||||
"localeRo": "Romænsk",
|
||||
"newMessageNotificationConversationInfo": "Ny Besked Fra %1",
|
||||
"newMessageNotificationSimple": "Ny Besked",
|
||||
"notificationContentContactInfo": "Samtaleinformation",
|
||||
|
@ -64,7 +118,6 @@
|
|||
"themeNameWitch": "Witch",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingDownloadFolder": "Download Folder",
|
||||
"settingImagePreviewsDescription": "Billeder vil automatisk blive hentet og vist. Vær opmærksom på at visning af billeder ofte fører til sikkerhedsmæssige sårbarheder og at du ikke bør aktivere dette eksperiment hvis du bruger Cwtch med kontakter du ikke har tillid til. Profilbilleder er planlagt til Cwtch 1.6.",
|
||||
"settingImagePreviews": "Automatisk billedvisning og profilbilleder",
|
||||
"experimentClickableLinksDescription": "Click-bare links eksperiment, der tillader dig at udveksle URL'er i samtaler",
|
||||
"enableExperimentClickableLinks": "Aktiver Click-bare Links",
|
||||
|
@ -81,7 +134,6 @@
|
|||
"importLocalServerSelectText": "Vælg lokal Server",
|
||||
"importLocalServerLabel": "Importer en lokalt administreret server",
|
||||
"newMessagesLabel": "Nye Beskeder",
|
||||
"localeRU": "Russisk",
|
||||
"copyServerKeys": "Kopier nøgler",
|
||||
"verfiyResumeButton": "Verificer\/genoptag",
|
||||
"fileCheckingStatus": "Kontroller download status",
|
||||
|
@ -141,7 +193,6 @@
|
|||
"settingUIColumnSingle": "Enkelt",
|
||||
"settingUIColumnLandscape": "UI Kolonner i landskabstilstand",
|
||||
"settingUIColumnPortrait": "UI Kolonner i portrættilstand",
|
||||
"localePl": "Polsk",
|
||||
"tooltipRemoveThisQuotedMessage": "Fjern citeret meddelelse.",
|
||||
"tooltipReplyToThisMessage": "Besvar denne meddelelse",
|
||||
"tooltipRejectContactRequest": "Afvis denne kontaktforespørgsel",
|
||||
|
@ -195,8 +246,6 @@
|
|||
"conversationSettings": "Samtaleindstillinger",
|
||||
"enterCurrentPasswordForDelete": "Indtast venligst nuværende password for at slette denne profil.",
|
||||
"enableGroups": "Aktiver Gruppe Chat",
|
||||
"localeIt": "Italiensk",
|
||||
"localeEs": "Spansk",
|
||||
"todoPlaceholder": "Todo...",
|
||||
"addNewItem": "Tilføj et nyt element til listen",
|
||||
"addListItem": "Tilføj en ny liste",
|
||||
|
@ -218,10 +267,6 @@
|
|||
"settingTheme": "Brug lyst tema",
|
||||
"largeTextLabel": "Stor",
|
||||
"settingInterfaceZoom": "Zoom niveau",
|
||||
"localeDe": "Tysk",
|
||||
"localePt": "Portugisisk",
|
||||
"localeFr": "Fransk",
|
||||
"localeEn": "Engelsk",
|
||||
"settingLanguage": "Sprog",
|
||||
"blockUnknownLabel": "Blokér ukendte kontakter",
|
||||
"zoomLabel": "Interface zoom (påvirker mest tekst- og knap-størrelser)",
|
||||
|
|
|
@ -1,317 +1,362 @@
|
|||
{
|
||||
"@@locale": "de",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"deleteConfirmLabel": "Gib LÖSCHEN ein, um zu bestätigen",
|
||||
"localeDa": "Dänisch",
|
||||
"localeCy": "Walisisch",
|
||||
"pasteAddressToAddContact": "Cwtch Adresse, Einladung oder Schlüssel hier hinzufügen, um eine neue Konversation hinzuzufügen",
|
||||
"titlePlaceholder": "Titel...",
|
||||
"peerName": "Name",
|
||||
"addPeerTab": "Einen Kontakt hinzufügen",
|
||||
"createGroupBtn": "Erstellen",
|
||||
"defaultGroupName": "Tolle Gruppe",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"localeIt": "Italienisch \/ Italiano",
|
||||
"errorDownloadDirectoryDoesNotExist": "Die Dateifreigabe kann nicht aktiviert werden, da der Download-Ordner nicht festgelegt wurde oder auf einen nicht vorhandenen Ordner festgelegt ist.",
|
||||
"localeTr": "Türkisch \/ Türk",
|
||||
"viewReplies": "Antworten auf diese Nachricht anzeigen",
|
||||
"manageSharedFiles": "Freigegebene Dateien verwalten",
|
||||
"tooltipPinConversation": "Konversation oben in \"Konversationen\" anheften",
|
||||
"tooltipUnpinConversation": "Anheften der Konversation oben in \"Konversationen\" aufheben",
|
||||
"messageNoReplies": "Es gibt keine Antworten auf diese Nachricht.",
|
||||
"stopSharingFile": "Beenden der Dateifreigabe",
|
||||
"restartFileShare": "Freigabe der Datei starten",
|
||||
"replyingTo": "Antwort auf %1",
|
||||
"fileDownloadUnavailable": "Diese Datei steht nicht zum Download zur Verfügung. Der Absender hat möglicherweise das Herunterladen dieser Datei deaktiviert.",
|
||||
"headingReplies": "Antworten",
|
||||
"localeEn": "Englisch \/ English",
|
||||
"localePl": "Polnisch \/ Polski",
|
||||
"localeRo": "Rumänisch \/ Română",
|
||||
"localeNo": "Norwegisch \/ Norsk",
|
||||
"localeLb": "Luxemburgisch \/ Lëtzebuergesch",
|
||||
"localeEl": "Griechisch\/ Ελληνικά",
|
||||
"localeEs": "Spanisch \/ Español",
|
||||
"localeRU": "Russisch \/ Русский",
|
||||
"localeDa": "Dänisch \/ Dansk",
|
||||
"localeFr": "Französisch \/ Français",
|
||||
"localeCy": "Walisisch \/ Cymraeg",
|
||||
"localePt": "Portugiesisch \/ Portuguesa",
|
||||
"settingImagePreviewsDescription": "Bilder und Profil-Bilder werden automatisch heruntergeladen und es wird eine Voransicht erstellt. Du solltest diese experimentelle Einstellung bei nicht vertrauenswürdigen Kontakten nicht aktivieren.",
|
||||
"localeDe": "Deutsch \/ Deutsch",
|
||||
"serverLabel": "Server",
|
||||
"createGroupTitle": "Gruppe erstellen",
|
||||
"torSettingsCustomControlPort": "Benutzerdefinierter Kontrollport",
|
||||
"torSettingsCustomControlPortDescription": "Verwende einen eigenen Port für Kontrollverbindungen zum Tor-Proxy",
|
||||
"torSettingsUseCustomTorServiceConfiguration": "Verwende eine eigene Tor-Service-Konfiguration (torrc)",
|
||||
"fileSharingSettingsDownloadFolderDescription": "Wenn Dateien automatisch heruntergeladen werden (z.B. Bilder, wenn Voransichten aktiviert ist), muss ein Ordner für Downloads eingestellt werden.",
|
||||
"torSettingsEnableCache": "Tor Konsensus zwischenspeichern",
|
||||
"torSettingsEnabledCacheDescription": "Zwischenspeichern des aktuell heruntergeladenen Tor-Konsenses, um ihn beim nächsten Öffnen von Cwtch wieder zu verwenden. Dies ermöglicht einen schnelleren Start von Tor. Wenn deaktiviert, löscht Cwtch die zwischengespeicherten Daten beim Starten.",
|
||||
"tooltipSelectACustomProfileImage": "Auswählen eines benutzerdefinierten Profilbilds",
|
||||
"notificationPolicyMute": "Stummschalten",
|
||||
"notificationPolicyOptIn": "Teilnahme zustimmen",
|
||||
"notificationPolicyDefaultAll": "Alle Einstellungen zurücksetzen",
|
||||
"conversationNotificationPolicyDefault": "Einstellung zurücksetzen",
|
||||
"conversationNotificationPolicyOptIn": "Teilnahme zustimmen",
|
||||
"notificationPolicySettingLabel": "Benachrichtigung Einstellungen",
|
||||
"notificationContentSettingLabel": "Inhalt der Benachrichtigung",
|
||||
"conversationNotificationPolicySettingDescription": "Steuerung des Benachrichtigungsverhaltens für diese Konversation",
|
||||
"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",
|
||||
"peerOfflineMessage": "Anderer Kontakt ist offline, Nachrichten können derzeit nicht zugestellt werden",
|
||||
"blockBtn": "Kontakt blockieren",
|
||||
"savePeerHistory": "Verlauf speichern",
|
||||
"savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll.",
|
||||
"dontSavePeerHistory": "Verlauf löschen",
|
||||
"unblockBtn": "Kontakt entsperren",
|
||||
"editProfile": "Profil bearbeiten",
|
||||
"blockUnknownLabel": "Unbekannte Kontakte blockieren",
|
||||
"networkStatusConnecting": "Verbindung mit Netzwerk und Kontakten...",
|
||||
"settingTheme": "Helles Farbschema benutzen",
|
||||
"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",
|
||||
"serverConnectionsLabel": "Verbindung",
|
||||
"serverTotalMessagesLabel": "Anzahl Mitteilungen",
|
||||
"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",
|
||||
"importLocalServerButton": "Importieren %1",
|
||||
"importLocalServerSelectText": "Lokalen Server auswählen",
|
||||
"importLocalServerLabel": "Lokalen Server auswählen",
|
||||
"newMessagesLabel": "Neue Nachrichten",
|
||||
"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",
|
||||
"serverDescriptionDescription": "Beschreibung für Server, diese Informationen wird nicht geteilt.",
|
||||
"serverDescriptionLabel": "Server Beschreibung",
|
||||
"serverAddress": "Server Adresse",
|
||||
"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",
|
||||
"tooltipRejectContactRequest": "Kontaktanfrage ablehnen",
|
||||
"tooltipAcceptContactRequest": "Kontaktanfrage annehmen.",
|
||||
"deleteBtn": "Löschen",
|
||||
"saveBtn": "Speichern",
|
||||
"membershipDescription": "Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben.",
|
||||
"newMessageNotificationConversationInfo": "Neue Nachricht von %1",
|
||||
"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",
|
||||
"yourServers": "Deine Server",
|
||||
"yourProfiles": "Deine Profile",
|
||||
"enterProfilePassword": "Gib ein Passwort ein, um deine Profile anzuzeigen",
|
||||
"notificationNewMessageFromGroup": "Neue Nachricht in einer Gruppe!",
|
||||
"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",
|
||||
"nickChangeSuccess": "Profilename erfolgreich geändert",
|
||||
"addServerFirst": "Du musst einen Server hinzufügen, bevor du eine Gruppe erstellen kannst",
|
||||
"deleteProfileSuccess": "Erfolgreich Profil gelöscht",
|
||||
"sendInvite": "Versende Kontakt- oder Gruppeneinladung",
|
||||
"sendMessage": "Nachricht senden",
|
||||
"cancel": "Abbrechen",
|
||||
"resetTor": "Zurücksetzen",
|
||||
"torStatus": "Tor Status",
|
||||
"torVersion": "Tor Version",
|
||||
"sendAnInvitation": "Du hast eine Einladung geschickt für: ",
|
||||
"rejected": "Abgelehnt!",
|
||||
"accepted": "Angenommen!",
|
||||
"chatHistoryDefault": "Diese Unterhaltung wird gelöscht sobald Cwtch geschlossen wird! Der Nachrichtenverlauf für jede Unterhaltung kann im Einstellungsmenü oben rechts geändert werden.",
|
||||
"newPassword": "Neues Passwort",
|
||||
"yesLeave": "Ja, diese Unterhaltung beenden",
|
||||
"reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Unterhaltung beenden möchtest? Alle Nachrichten und Attribute werden gelöscht.",
|
||||
"leaveConversation": "Unterhaltung beenden",
|
||||
"inviteToGroup": "Du wurdest eingeladen einer Gruppe beizutreten:",
|
||||
"titleManageServers": "Server verwalten",
|
||||
"successfullAddedContact": "Erfolgreich hinzugefügt",
|
||||
"titleManageProfiles": "Cwtch Profile verwalten",
|
||||
"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",
|
||||
"displayNameLabel": "Angezeigename",
|
||||
"dmTooltip": "Klicken, um Direktnachricht zu senden",
|
||||
"searchList": "Liste durchsuchen",
|
||||
"update": "Update",
|
||||
"viewServerInfo": "Serverinfo",
|
||||
"serverNotSynced": "Neue Nachrichten abrufen (Dies kann eine Weile dauern...)",
|
||||
"serverSynced": "synchronisiert",
|
||||
"todoPlaceholder": "noch zu erledigen",
|
||||
"addListItem": "Liste hinzufügen",
|
||||
"addNewItem": "Ein neues Element zur Liste hinzufügen",
|
||||
"defaultGroupName": "Fantastische Gruppe",
|
||||
"createGroupBtn": "Erstellen",
|
||||
"profileOnionLabel": "Sende diese Adresse an Personen, mit denen Du in Kontakt treten möchtest",
|
||||
"addPeerTab": "Einen Kontakt hinzufügen",
|
||||
"createGroupTab": "Eine Gruppe erstellen",
|
||||
"joinGroupTab": "Einer Gruppe beitreten",
|
||||
"peerAddress": "Adresse",
|
||||
"joinGroupTab": "Einer Gruppe beitreten",
|
||||
"server": "Server",
|
||||
"invitation": "Einladung",
|
||||
"peerName": "Name",
|
||||
"groupAddr": "Adresse",
|
||||
"createGroup": "Gruppe erstellen",
|
||||
"invitation": "Einladung",
|
||||
"addPeer": "Kontakt hinzufügen",
|
||||
"joinGroup": "Gruppe beitreten",
|
||||
"blocked": "Blockiert",
|
||||
"createGroup": "Gruppe erstellen",
|
||||
"newBulletinLabel": "Neue Bekanntmachung",
|
||||
"titlePlaceholder": "Titel...",
|
||||
"postNewBulletinLabel": "Neue Bekanntmachung veröffentlichen",
|
||||
"pasteAddressToAddContact": "Füge hier eine cwtch-Adresse, eine Einladung oder ein Schlüsselpaket ein, um eine neue Unterhaltung hinzuzufügen",
|
||||
"search": "Suche...",
|
||||
"blocked": "Blockiert",
|
||||
"invitationLabel": "Einladung",
|
||||
"serverInfo": "Server-Informationen",
|
||||
"serverConnectivityConnected": "Server verbunden",
|
||||
"serverConnectivityDisconnected": "Server getrennt",
|
||||
"serverNotSynced": "Neue Nachrichten synchronisieren (dies kann einige Zeit dauern)...",
|
||||
"serverSynced": "Synchronisiert",
|
||||
"groupNameLabel": "Gruppenname",
|
||||
"viewServerInfo": "Server-Info",
|
||||
"inviteToGroupLabel": "Zur Gruppe einladen",
|
||||
"saveBtn": "Speichern",
|
||||
"deleteBtn": "Löschen",
|
||||
"inviteBtn": "Einladen",
|
||||
"update": "Update",
|
||||
"peerNotOnline": "Der Kontakt ist offline. Anwendungen können im Moment nicht verwendet werden.",
|
||||
"searchList": "Liste durchsuchen",
|
||||
"addListItemBtn": "Element hinzufügen",
|
||||
"addProfileTitle": "Neues Profil hinzufügen",
|
||||
"membershipDescription": "Nachfolgend findest Du eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Diese Liste gibt möglicherweise nicht alle Benutzer wieder, die Zugang zu der Gruppe haben.",
|
||||
"dmTooltip": "Klicken für Direktnachricht",
|
||||
"couldNotSendMsgError": "Diese Nachricht konnte nicht gesendet werden",
|
||||
"acknowledgedLabel": "Bestätigt",
|
||||
"peerBlockedMessage": "Kontakt ist blockiert",
|
||||
"pendingLabel": "Anhängig",
|
||||
"peerOfflineMessage": "Kontakt ist offline, Nachrichten können im Moment nicht zugestellt werden",
|
||||
"copyBtn": "Kopieren",
|
||||
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen zu",
|
||||
"newGroupBtn": "Neue Gruppe erstellen",
|
||||
"acceptGroupBtn": "Annehmen",
|
||||
"chatBtn": "Chat",
|
||||
"rejectGroupBtn": "Ablehnen",
|
||||
"listsBtn": "Listen",
|
||||
"puzzleGameBtn": "Puzzlespiel",
|
||||
"bulletinsBtn": "Bekanntmachungen",
|
||||
"addressLabel": "Adresse",
|
||||
"copiedToClipboardNotification": "In die Zwischenablage kopiert",
|
||||
"displayNameLabel": "Anzeigename",
|
||||
"blockBtn": "Kontakt blockieren",
|
||||
"savePeerHistory": "Verlauf speichern",
|
||||
"savePeerHistoryDescription": "Legt fest, ob der mit dem Kontakt verbundene Verlauf gelöscht werden soll.",
|
||||
"dontSavePeerHistory": "Verlauf löschen",
|
||||
"unblockBtn": "Kontakt entsperren",
|
||||
"editProfileTitle": "Profil bearbeiten",
|
||||
"addProfileTitle": "Neues Profil hinzufügen",
|
||||
"profileName": "Anzeigename",
|
||||
"defaultProfileName": "Alice",
|
||||
"editProfile": "Profil bearbeiten",
|
||||
"newProfile": "Neues Profil",
|
||||
"radioUsePassword": "Passwort",
|
||||
"radioNoPassword": "Unverschlüsselt (kein Passwort)",
|
||||
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.",
|
||||
"yourDisplayName": "Dein Anzeigename",
|
||||
"currentPasswordLabel": "Aktuelles Passwort",
|
||||
"password2Label": "Passwort erneut eingeben",
|
||||
"password1Label": "Passwort",
|
||||
"passwordErrorEmpty": "Passwort darf nicht leer sein",
|
||||
"createProfileBtn": "Profil erstellen",
|
||||
"passwordErrorMatch": "Passwörter stimmen nicht überein",
|
||||
"saveProfileBtn": "Profil speichern",
|
||||
"passwordChangeError": "Fehler beim Ändern des Passworts: Eingegebenes Passwort abgelehnt",
|
||||
"deleteConfirmLabel": "Gib LÖSCHEN ein, um zu bestätigen",
|
||||
"deleteProfileBtn": "Profil löschen",
|
||||
"deleteConfirmText": "LÖSCHEN",
|
||||
"deleteProfileConfirmBtn": "Profil wirklich löschen",
|
||||
"addNewProfileBtn": "Neues Profil hinzufügen",
|
||||
"newConnectionPaneTitle": "Neue Verbindung",
|
||||
"password1Label": "Passwort",
|
||||
"password2Label": "Passwort erneut eingeben",
|
||||
"createProfileBtn": "Profil speichern",
|
||||
"saveProfileBtn": "Profil speichern",
|
||||
"passwordErrorMatch": "Passwörter stimmen nicht überein",
|
||||
"passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt",
|
||||
"deleteConfirmText": "LÖSCHEN",
|
||||
"password": "Passwort",
|
||||
"enterProfilePassword": "Gib ein Passwort ein, um deine Profile anzuzeigen",
|
||||
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
|
||||
"yourProfiles": "Deine Profile",
|
||||
"yourServers": "Deine Server",
|
||||
"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",
|
||||
"networkStatusOnline": "Online",
|
||||
"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",
|
||||
"puzzleGameBtn": "Puzzlespiel",
|
||||
"bulletinsBtn": "Meldungen",
|
||||
"listsBtn": "Listen",
|
||||
"chatBtn": "Chat",
|
||||
"rejectGroupBtn": "Ablehnen",
|
||||
"acceptGroupBtn": "Annehmen",
|
||||
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen",
|
||||
"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",
|
||||
"groupNameLabel": "Gruppenname",
|
||||
"invitationLabel": "Einladung",
|
||||
"serverLabel": "Server",
|
||||
"postNewBulletinLabel": "Neue Meldung veröffentlichen",
|
||||
"newBulletinLabel": "Neue Meldung"
|
||||
"versionBuilddate": "Version: %1 Erstellt am: %2",
|
||||
"settingLanguage": "Sprache",
|
||||
"settingInterfaceZoom": "Zoomstufe",
|
||||
"largeTextLabel": "Groß",
|
||||
"themeLight": "Hell",
|
||||
"settingTheme": "Helles Farbschema benutzen",
|
||||
"themeDark": "Dunkel",
|
||||
"experimentsEnabled": "Experimentelle Funktionen aktivieren",
|
||||
"version": "Version %1",
|
||||
"versionTor": "Version %1 mit Tor %2",
|
||||
"builddate": "Erstellt am: %2",
|
||||
"defaultScalingText": "Text in Standardgröße (Skalierungsfaktor:",
|
||||
"smallTextLabel": "Klein",
|
||||
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
|
||||
"loadingTor": "Tor wird geladen...",
|
||||
"networkStatusDisconnected": "Verbindung zum Internet unterbrochen, überprüfe Deine Verbindung",
|
||||
"networkStatusAttemptingTor": "Versuch, eine Verbindung zum Tor-Netzwerk herzustellen",
|
||||
"networkStatusConnecting": "Verbindung mit Netzwerk und Kontakten...",
|
||||
"networkStatusOnline": "Online",
|
||||
"newConnectionPaneTitle": "Neue Verbindung",
|
||||
"addNewItem": "Hinzufügen eines neuen Elements zur Liste",
|
||||
"addListItem": "Hinzufügen eines neuen Listenelements",
|
||||
"todoPlaceholder": "noch zu erledigen...",
|
||||
"enterCurrentPasswordForDelete": "Bitte gib das aktuelle Passwort ein, um dieses Profil zu löschen.",
|
||||
"enableGroups": "Gruppenchat aktivieren",
|
||||
"conversationSettings": "Konversationseinstellungen",
|
||||
"invalidImportString": "Ungültige Importzeichenfolge",
|
||||
"tooltipOpenSettings": "Öffne das Einstellungsmenü",
|
||||
"contactAlreadyExists": "Kontakt existiert bereits",
|
||||
"titleManageContacts": "Konversationen",
|
||||
"tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen",
|
||||
"tooltipUnlockProfiles": "Entsperre verschlüsselte Profile durch Eingabe des Passworts.",
|
||||
"titleManageProfiles": "Cwtch Profile verwalten",
|
||||
"descriptionExperiments": "Bei den Cwtch-Experimenten handelt es sich um optionale Opt-in-Funktionen, die Cwtch um zusätzliche Funktionen erweitern, bei denen der Datenschutz anders berücksichtigt werden kann als beim traditionellen 1:1-Chat mit Metadaten, z. B. Gruppenchat, Bot-Integration usw.",
|
||||
"descriptionExperimentsGroups": "Das Experiment Gruppen ermöglicht es Cwtch, sich mit einer nicht vertrauenswürdigen Serverinfrastruktur zu verbinden, um die Kommunikation mit mehr als einem Kontakt zu erleichtern.",
|
||||
"descriptionBlockUnknownConnections": "Wenn diese Option aktiviert ist, werden Verbindungen von Cwtch-Benutzern, die nicht zu Deiner Kontaktliste hinzugefügt wurden, automatisch geschlossen.",
|
||||
"successfullAddedContact": "Erfolgreich hinzugefügt",
|
||||
"titleManageServers": "Server verwalten",
|
||||
"inviteToGroup": "Du wurdest eingeladen einer Gruppe beizutreten:",
|
||||
"reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Konversation verlassen möchtest? Alle Nachrichten und Attribute werden gelöscht.",
|
||||
"leaveConversation": "Diese Konversation verlassen",
|
||||
"yesLeave": "Ja, diese Konversation verlassen",
|
||||
"newPassword": "Neues Passwort",
|
||||
"chatHistoryDefault": "Diese Konversation wird gelöscht, wenn Cwtch geschlossen wird! Der Nachrichtenverlauf kann für jedes Gespräch über das Menü Einstellungen oben rechts aktiviert werden.",
|
||||
"accepted": "Angenommen!",
|
||||
"rejected": "Abgelehnt!",
|
||||
"contactSuggestion": "Dies ist ein Kontaktvorschlag für: ",
|
||||
"sendAnInvitation": "Du hast eine Einladung geschickt für: ",
|
||||
"torStatus": "Tor-Status",
|
||||
"torVersion": "Tor-Version",
|
||||
"resetTor": "Zurücksetzen",
|
||||
"sendMessage": "Nachricht senden",
|
||||
"cancel": "Abbrechen",
|
||||
"sendInvite": "Sende eine Kontakt- oder Gruppeneinladung",
|
||||
"addServerFirst": "Du musst einen Server hinzufügen, bevor du eine Gruppe erstellen kannst",
|
||||
"deleteProfileSuccess": "Erfolgreich Profil gelöscht",
|
||||
"nickChangeSuccess": "Profilename erfolgreich geändert",
|
||||
"createProfileToBegin": "Bitte erstelle oder entsperre ein Profil, um zu beginnen",
|
||||
"addContactFirst": "Füge einen Kontakt hinzu oder wähle einen aus, um den Chat zu beginnen.",
|
||||
"debugLog": "Aktivieren der Konsolendebugprotokollierung",
|
||||
"torNetworkStatus": "Tor Netzwerkstatus",
|
||||
"profileDeleteSuccess": "Profil erfolgreich gelöscht",
|
||||
"malformedMessage": "Fehlerhafte Nachricht",
|
||||
"shutdownCwtchTooltip": "Cwtch herunterfahren",
|
||||
"shutdownCwtchDialog": "Bist Du sicher, dass Du Cwtch herunterfahren möchtest? Damit werden alle Verbindungen geschlossen und die Anwendung beendet.",
|
||||
"shutdownCwtchDialogTitle": "Cwtch herunterfahren?",
|
||||
"shutdownCwtchAction": "Cwtch herunterfahren",
|
||||
"groupInviteSettingsWarning": "Sie wurden eingeladen, einer Gruppe beizutreten! Bitte aktiviere das Gruppenchat-Experiment in den Einstellungen, um diese Einladung anzuzeigen.",
|
||||
"tooltipShowPassword": "Password anzeigen",
|
||||
"notificationNewMessageFromPeer": "Neue Nachricht von einem Kontakt!",
|
||||
"tooltipHidePassword": "Passwort verbergen",
|
||||
"notificationNewMessageFromGroup": "Neue Nachricht in einer Gruppe!",
|
||||
"tooltipAcceptContactRequest": "Akzeptiere diese Kontaktanfrage.",
|
||||
"tooltipRejectContactRequest": "Diese Kontaktanfrage ablehnen",
|
||||
"tooltipReplyToThisMessage": "Auf diese Nachricht antworten",
|
||||
"tooltipRemoveThisQuotedMessage": "Zitierte Nachricht entfernen.",
|
||||
"settingUIColumnLandscape": "UI-Spalten im Querformat",
|
||||
"settingUIColumnPortrait": "UI-Spalten im Hochformat",
|
||||
"settingUIColumnDouble12Ratio": "Doppelt (1:2)",
|
||||
"settingUIColumnSingle": "Einzeln",
|
||||
"settingUIColumnDouble14Ratio": "Doppelt (1:4)",
|
||||
"settingUIColumnOptionSame": "Identisch mit der Hochformat-Einstellung",
|
||||
"contactGoto": "Zur Unterhaltung mit %1 wechseln",
|
||||
"zoomLabel": "Benutzeroberflächen-Zoom (betrifft hauptsächlich Text- und Button-Größen)",
|
||||
"blockUnknownLabel": "Unbekannte Kontakte blockieren",
|
||||
"addContact": "Kontakt hinzufügen",
|
||||
"addContactConfirm": "Kontakt hinzufügen %1",
|
||||
"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.",
|
||||
"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.",
|
||||
"placeholderEnterMessage": "Gib eine Nachricht ein...",
|
||||
"blockedMessageMessage": "Diese Nachricht stammt von einem Profil, das Du blockiert hast.",
|
||||
"showMessageButton": "Nachricht anzeigen",
|
||||
"blockUnknownConnectionsEnabledDescription": "Verbindungen von unbekannten Kontakten werden blockiert. Du kannst dies in den Einstellungen ändern.",
|
||||
"archiveConversation": "Archiviere diese Konversation",
|
||||
"streamerModeLabel": "Streamer\/Präsentationsmodus",
|
||||
"descriptionStreamerMode": "Wenn diese Option aktiviert ist, wird die App für das Streaming oder die Präsentation optisch privater gestaltet, indem z. B. Profil- und Kontaktadressen ausgeblendet werden.",
|
||||
"retrievingManifestMessage": "Dateiinformationen werden abgerufen...",
|
||||
"openFolderButton": "Ordner öffnen",
|
||||
"downloadFileButton": "Herunterladen",
|
||||
"labelFilesize": "Größe",
|
||||
"labelFilename": "Dateiname",
|
||||
"messageEnableFileSharing": "Aktiviere das Dateifreigabe-Experiment, um diese Nachricht anzuzeigen.",
|
||||
"messageFileSent": "Du hast eine Datei gesendet",
|
||||
"messageFileOffered": "Kontakt bietet an, Dir eine Datei zu schicken",
|
||||
"tooltipSendFile": "Datei senden",
|
||||
"settingFileSharing": "Gemeinsame Nutzung von Dateien",
|
||||
"descriptionFileSharing": "Mit dem Dateifreigabe-Experiment kannst Du Dateien von Cwtch-Kontakten und -Gruppen senden und empfangen. Beachte, dass das Teilen einer Datei mit einer Gruppe dazu führt, dass sich die Mitglieder dieser Gruppe direkt mit Dir über Cwtch verbinden, um die Datei herunterzuladen.",
|
||||
"titleManageProfilesShort": "Profile",
|
||||
"addServerTitle": "Server hinzufügen",
|
||||
"editServerTitle": "Server bearbeiten",
|
||||
"serverAddress": "Server-Adresse",
|
||||
"serverDescriptionLabel": "Server-Beschreibung",
|
||||
"serverDescriptionDescription": "Deine Beschreibung des Servers ist nur für die persönliche Verwaltung bestimmt und wird nicht weitergegeben.",
|
||||
"serverEnabled": "Server aktiviert",
|
||||
"serverEnabledDescription": "Server starten oder stoppen",
|
||||
"serverAutostartLabel": "Autostart",
|
||||
"serverAutostartDescription": "Legt fest, ob die Anwendung den Server beim Start automatisch starten soll",
|
||||
"saveServerButton": "Server sichern",
|
||||
"serversManagerTitleLong": "Von Dir gehostete Server",
|
||||
"serversManagerTitleShort": "Server",
|
||||
"addServerTooltip": "Neuen Server hinzufügen",
|
||||
"unlockServerTip": "Bitte entsperre oder erstelle einen Server, um zu beginnen!",
|
||||
"unlockProfileTip": "Bitte erstelle oder entsperre ein Profil, um zu beginnen!",
|
||||
"enterServerPassword": "Passwort um Server zu entsperren",
|
||||
"settingServers": "Server hosten",
|
||||
"settingServersDescription": "Das Experiment \"Hosting-Server\" ermöglicht das Hosting und die Verwaltung von Cwtch-Servern",
|
||||
"copyAddress": "Adresse kopieren",
|
||||
"enterCurrentPasswordForDeleteServer": "Bitte gib das aktuelle Passwort ein, um diesen Server zu löschen",
|
||||
"deleteServerSuccess": "Server erfolgreich gelöscht",
|
||||
"deleteServerConfirmBtn": "Server wirklich löschen",
|
||||
"plainServerDescription": "Wir empfehlen, dass du deinen Cwtch-Server mit einem Passwort schützst. Wenn Du diesen Server nicht mit einem Kennwort versiehst, kann jeder, der Zugang zu diesem Gerät hat, auf Informationen über diesen Server zugreifen, einschließlich sensibler kryptografischer Schlüssel.",
|
||||
"encryptedServerDescription": "Das Verschlüsseln eines Servers mit einem Kennwort schützt ihn vor anderen Personen, die dieses Gerät ebenfalls benutzen könnten. Verschlüsselte Server können nicht entschlüsselt, angezeigt oder aufgerufen werden, bis das richtige Kennwort eingegeben wird, um sie zu entsperren.",
|
||||
"fileSavedTo": "Gesichert in",
|
||||
"fileInterrupted": "Unterbrochen",
|
||||
"fileCheckingStatus": "Überprüfen des Download-Status",
|
||||
"verfiyResumeButton": "Überprüfen\/Fortsetzen",
|
||||
"copyServerKeys": "Schlüssel kopieren",
|
||||
"newMessagesLabel": "Neue Nachrichten",
|
||||
"importLocalServerLabel": "Importieren eines lokal gehosteten Servers",
|
||||
"importLocalServerSelectText": "Lokalen Server auswählen",
|
||||
"importLocalServerButton": "%1 importieren",
|
||||
"groupsOnThisServerLabel": "Gruppen, in denen ich bin, werden auf diesem Server gehostet",
|
||||
"fieldDescriptionLabel": "Beschreibung",
|
||||
"manageKnownServersButton": "Bekannte Server verwalten",
|
||||
"displayNameTooltip": "Bitte gib einen Anzeigenamen ein",
|
||||
"manageKnownServersLong": "Bekannte Server verwalten",
|
||||
"manageKnownServersShort": "Server",
|
||||
"serverMetricsLabel": "Server-Metriken",
|
||||
"serverTotalMessagesLabel": "Nachrichten insgesamt",
|
||||
"serverConnectionsLabel": "Verbindung",
|
||||
"enableExperimentClickableLinks": "Anklickbare Links aktivieren",
|
||||
"experimentClickableLinksDescription": "Das Experiment mit anklickbaren Links ermöglicht es dir, auf URLs zu klicken, die in Nachrichten geteilt werden",
|
||||
"settingImagePreviews": "Bild Voransichten und Profil Bilder",
|
||||
"settingDownloadFolder": "Download Ordner",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"themeNameWitch": "Hexe",
|
||||
"themeNameVampire": "Vampir",
|
||||
"themeNameGhost": "Geist",
|
||||
"themeNamePumpkin": "Kürbis",
|
||||
"themeNameMidnight": "Mitternacht",
|
||||
"themeNameMermaid": "Meerjungfrau",
|
||||
"themeNameNeon1": "Neon1",
|
||||
"themeColorLabel": "Farbthema",
|
||||
"themeNameNeon2": "Neon2",
|
||||
"loadingCwtch": "Lade Cwtch...",
|
||||
"storageMigrationModalMessage": "Profile in ein neues Speicherformat migrieren. Das kann ein paar Minuten dauern...",
|
||||
"msgFileTooBig": "Dateigröße darf nicht größer als 10 GB sein",
|
||||
"msgConfirmSend": "Möchtest Du diese Datei wirklich senden",
|
||||
"btnSendFile": "Datei senden",
|
||||
"msgAddToAccept": "Füge dieses Konto zu Deinen Kontakten hinzu, um diese Datei zu akzeptieren.",
|
||||
"torSettingsEnabledAdvanced": "Erweiterte Tor-Konfiguration aktivieren",
|
||||
"torSettingsEnabledAdvancedDescription": "Verwende einen bestehenden Tor-Dienst auf deinem System oder ändere die Parameter des Cwtch Tor Services",
|
||||
"torSettingsCustomSocksPort": "Benutzerdefinierter SOCKS-Port",
|
||||
"torSettingsCustomSocksPortDescription": "Verwende einen eigenen Port für Datenverbindungen zum Tor-Proxy",
|
||||
"torSettingsCustomControlPort": "Benutzerdefinierter Kontrollport",
|
||||
"torSettingsCustomControlPortDescription": "Verwende einen eigenen Port für Kontrollverbindungen zum Tor-Proxy",
|
||||
"torSettingsUseCustomTorServiceConfiguration": "Verwende eine eigene Tor-Service-Konfiguration (torrc)",
|
||||
"torSettingsUseCustomTorServiceConfigurastionDescription": "Überschreibe die Standard-Tor-Konfiguration. Warnung: Dies könnte gefährlich sein. Schalte dies nur ein, wenn Du weißt, was Du machst.",
|
||||
"torSettingsErrorSettingPort": "Port Nummer muss zwischen 1 und 65535 liegen",
|
||||
"fileSharingSettingsDownloadFolderDescription": "Wenn Dateien automatisch heruntergeladen werden (z. B. Bilddateien, wenn die Bildvorschau aktiviert ist), wird ein Standardspeicherort benötigt, an den die Dateien heruntergeladen werden.",
|
||||
"fileSharingSettingsDownloadFolderTooltip": "Durchsuchen, um einen anderen Standardordner für heruntergeladene Dateien auszuwählen.",
|
||||
"labelACNCircuitInfo": "ACN Circuit Information",
|
||||
"descriptionACNCircuitInfo": "Ausführliche Informationen über den Pfad, den das anonyme Kommunikationsnetzwerk verwendet, um eine Verbindung mit dieser Konversation herzustellen.",
|
||||
"labelTorNetwork": "Tor Netzwerk",
|
||||
"torSettingsEnableCache": "Tor Konsensus zwischenspeichern",
|
||||
"torSettingsEnabledCacheDescription": "Zwischenspeichern des aktuell heruntergeladenen Tor-Konsenses, um ihn beim nächsten Öffnen von Cwtch wiederzuverwenden. Dies ermöglicht einen schnelleren Start von Tor. Wenn deaktiviert, löscht Cwtch die zwischengespeicherten Daten beim Starten.",
|
||||
"tooltipSelectACustomProfileImage": "Auswählen eines benutzerdefinierten Profilbilds",
|
||||
"notificationPolicyMute": "Stummschalten",
|
||||
"notificationPolicyOptIn": "Annehmen",
|
||||
"notificationPolicyDefaultAll": "Standard Alle",
|
||||
"conversationNotificationPolicyDefault": "Standard",
|
||||
"conversationNotificationPolicyOptIn": "Annehmen",
|
||||
"notificationPolicySettingLabel": "Benachrichtigungsrichtlinie",
|
||||
"conversationNotificationPolicyNever": "Niemals",
|
||||
"notificationPolicySettingDescription": "Steuert das standardmäßige Benachrichtigungsverhalten der Anwendungsbenachrichtigungsverhalten",
|
||||
"notificationContentSettingLabel": "Inhalt der Benachrichtigung",
|
||||
"notificationContentSettingDescription": "Steuert den Inhalt von Konversationsbenachrichtigungen",
|
||||
"settingGroupBehaviour": "Verhalten",
|
||||
"settingsGroupAppearance": "Aussehen",
|
||||
"settingsGroupExperiments": "Experimente",
|
||||
"conversationNotificationPolicySettingLabel": "Richtlinie für Konversationsbenachrichtigungen",
|
||||
"conversationNotificationPolicySettingDescription": "Steuerung des Benachrichtigungsverhaltens für diese Konversation",
|
||||
"notificationContentSimpleEvent": "Einfaches Ereignis",
|
||||
"notificationContentContactInfo": "Informationen zur Konversationseinstellungen",
|
||||
"newMessageNotificationSimple": "Neue Nachricht",
|
||||
"newMessageNotificationConversationInfo": "Neue Nachricht von %1",
|
||||
"exportProfile": "Profil exportieren",
|
||||
"exportProfileTooltip": "Sichern des Profils in eine verschlüsselte Datei. Die verschlüsselte Datei kann in eine andere Cwtch App importiert werden.",
|
||||
"importProfile": "Profil importieren",
|
||||
"importProfileTooltip": "Benutze ein verschlüsseltes Cwtch Backup, um ein in einer anderen Cwtch Instanz erzeugtes Profil zu aktivieren.",
|
||||
"failedToImportProfile": "Fehler beim Import des Profils",
|
||||
"successfullyImportedProfile": "Profil erfolgreich importiert: %profile",
|
||||
"shuttingDownApp": "Herunterfahren ...",
|
||||
"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?",
|
||||
"clickableLinkOpen": "URL öffnen",
|
||||
"clickableLinksCopy": "URL kopieren",
|
||||
"clickableLinkError": "Beim Versuch, die URL zu öffnen, ist ein Fehler aufgetreten",
|
||||
"formattingExperiment": "Formatierung von Nachrichtenbearbeitung",
|
||||
"messageFormattingDescription": "Aktiviere Richtext Formatierung in den angezeigten Nachrichten z.B. **fett** und *kursiv*",
|
||||
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Für diese Funktion muss das Gruppen-Experiment in den Einstellungen aktiviert sein.",
|
||||
"settingAndroidPowerExemption": "Ignoriere Android Akku-Optimierungen",
|
||||
"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.",
|
||||
"settingsAndroidPowerReenablePopup": "Die Akku-Optimierung kann in Cwtch nicht wieder aktiviert werden. Bitte gehe zu Android \/ Einstellungen \/ Apps \/ Cwtch \/ Batterie und setze die Nutzung auf \"Optimiert\".",
|
||||
"okButton": "OK",
|
||||
"tooltipBoldText": "Fett",
|
||||
"tooltipBackToMessageEditing": "Zurück zu Nachrichtenbearbeitung",
|
||||
"tooltipItalicize": "Kursiv",
|
||||
"tooltipSuperscript": "Hochgestellt",
|
||||
"tooltipSubscript": "Tiefgestellt",
|
||||
"tooltipStrikethrough": "Durchgestrichen",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipPreviewFormatting": "Vorschau Nachrichtenformatierung"
|
||||
}
|
|
@ -1,8 +1,66 @@
|
|||
{
|
||||
"@@locale": "el",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"localeCy": "Ουαλικά",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Τουρκικά \/ Türk",
|
||||
"localeIt": "Italian \/ Italiano",
|
||||
"localeCy": "Ουαλικά \/ Cymraeg",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Γερμανός \/ Deutsch",
|
||||
"localeEn": "English \/ English",
|
||||
"localeLb": "Λουξεμβουργιανά",
|
||||
"localeNo": "Νορβηγικά",
|
||||
"localeEl": "Ελληνικά",
|
||||
"localePl": "Polish \/ Polski",
|
||||
"localeRo": "Ρουμανικά",
|
||||
"localeRU": "Ρωσικά",
|
||||
"localeEs": "Spanish \/ Español",
|
||||
"localeDa": "Δανικά",
|
||||
"localePt": "Portuguese \/ Portuguesa",
|
||||
"localeFr": "French \/ Français",
|
||||
"settingImagePreviewsDescription": "Θα γίνει αυτόματη λήψη και προεπισκόπηση των εικόνων. Λάβετε υπόψη ότι οι προεπισκοπήσεις εικόνων μπορεί συχνά να οδηγήσουν σε ευπάθειες ασφαλείας και δεν θα πρέπει να ενεργοποιήσετε αυτό το πείραμα εάν χρησιμοποιείτε το Cwtch με μη αξιόπιστες επαφές. Οι φωτογραφίες προφίλ έχουν προγραμματιστεί για το Cwtch 1.6.",
|
||||
"tooltipPreviewFormatting": "Preview Message Formatting",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipItalicize": "Italic",
|
||||
"tooltipBackToMessageEditing": "Back to Message Editing",
|
||||
"tooltipBoldText": "Bold",
|
||||
"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",
|
||||
"server": "Διακομιστής",
|
||||
"peerName": "Όνομα",
|
||||
"peerAddress": "Διεύθυνση",
|
||||
|
@ -47,7 +105,6 @@
|
|||
"fileCheckingStatus": "Έλεγχος κατάστασης λήψης",
|
||||
"verfiyResumeButton": "Επαλήθευση\/συνέχιση",
|
||||
"copyServerKeys": "Αντιγραφή κλειδιών",
|
||||
"localeRU": "Ρωσικά",
|
||||
"newMessagesLabel": "Νέα μηνύματα",
|
||||
"importLocalServerLabel": "Εισαγωγή ενός τοπικά φιλοξενούμενου διακομιστή",
|
||||
"importLocalServerSelectText": "Επιλογή τοπικού διακομιστή",
|
||||
|
@ -62,7 +119,6 @@
|
|||
"fileSharingSettingsDownloadFolderTooltip": "Επιλέξετε έναν διαφορετικό προεπιλεγμένο φάκελο για τα αρχεία που έχουν ληφθεί.",
|
||||
"torSettingsEnableCache": "Αποθήκευση Tor Consensus",
|
||||
"settingImagePreviews": "Προεπισκοπήσεις εικόνων και εικόνες προφίλ",
|
||||
"settingImagePreviewsDescription": "Θα γίνει αυτόματη λήψη και προεπισκόπηση των εικόνων. Λάβετε υπόψη ότι οι προεπισκοπήσεις εικόνων μπορεί συχνά να οδηγήσουν σε ευπάθειες ασφαλείας και δεν θα πρέπει να ενεργοποιήσετε αυτό το πείραμα εάν χρησιμοποιείτε το Cwtch με μη αξιόπιστες επαφές. Οι φωτογραφίες προφίλ έχουν προγραμματιστεί για το Cwtch 1.6.",
|
||||
"settingDownloadFolder": "Φάκελος Λήψης",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"themeNameNeon1": "Neon1",
|
||||
|
@ -83,10 +139,6 @@
|
|||
"notificationContentContactInfo": "Πληροφορίες συνομιλίας",
|
||||
"newMessageNotificationSimple": "Νέο μήνυμα",
|
||||
"newMessageNotificationConversationInfo": "Νέο μήνυμα από %1",
|
||||
"localeRo": "Ρουμανικά",
|
||||
"localeLb": "Λουξεμβουργιανά",
|
||||
"localeNo": "Νορβηγικά",
|
||||
"localeEl": "Ελληνικά",
|
||||
"notificationContentSimpleEvent": "Plain Event",
|
||||
"conversationNotificationPolicySettingDescription": "Control notification behaviour for this conversation",
|
||||
"conversationNotificationPolicySettingLabel": "Conversation Notification Policy",
|
||||
|
@ -146,7 +198,6 @@
|
|||
"settingUIColumnSingle": "Single",
|
||||
"settingUIColumnLandscape": "UI Columns in Landscape Mode",
|
||||
"settingUIColumnPortrait": "UI Columns in Portrait Mode",
|
||||
"localePl": "Polish",
|
||||
"tooltipRemoveThisQuotedMessage": "Remove quoted message.",
|
||||
"tooltipReplyToThisMessage": "Reply to this message",
|
||||
"tooltipRejectContactRequest": "Reject this contact request",
|
||||
|
@ -199,8 +250,6 @@
|
|||
"invalidImportString": "Invalid import string",
|
||||
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
|
||||
"enableGroups": "Enable Group Chat",
|
||||
"localeIt": "Italiana",
|
||||
"localeEs": "Espanol",
|
||||
"todoPlaceholder": "Todo...",
|
||||
"addNewItem": "Add a new item to the list",
|
||||
"addListItem": "Add a New List Item",
|
||||
|
@ -222,10 +271,6 @@
|
|||
"settingTheme": "Use Light Themes",
|
||||
"largeTextLabel": "Large",
|
||||
"settingInterfaceZoom": "Zoom level",
|
||||
"localeDe": "Deutsche",
|
||||
"localePt": "Portuguesa",
|
||||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Language",
|
||||
"blockUnknownLabel": "Block Unknown Contacts",
|
||||
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
|
||||
|
|
|
@ -1,13 +1,67 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"localeDa": "Danish",
|
||||
"localeCy": "Welsh",
|
||||
"localeEl": "Greek",
|
||||
"localeNo": "Norwegian",
|
||||
"localeLb": "Luxembourgish",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeIt": "Italian \/ Italiano",
|
||||
"localeTr": "Turkish \/ Türk",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "German \/ Deutsch",
|
||||
"localeEn": "English \/ English",
|
||||
"localeLb": "Luxembourgish \/ Lëtzebuergesch",
|
||||
"localeNo": "Norwegian \/ Norsk",
|
||||
"localeEl": "Greek \/ Ελληνικά",
|
||||
"localePl": "Polish \/ Polski",
|
||||
"localeRo": "Romanian \/ Română",
|
||||
"localeRU": "Russian \/ Русский",
|
||||
"localeEs": "Spanish \/ Español",
|
||||
"localeDa": "Danish \/ Dansk",
|
||||
"localePt": "Portuguese \/ Portuguesa",
|
||||
"localeFr": "French \/ Français",
|
||||
"localeCy": "Welsh \/ Cymraeg",
|
||||
"settingImagePreviewsDescription": "Images and Profile Pictures will be downloaded and previewed automatically. We recommend that you do not enable this Experiment if you use Cwtch with untrusted contacts.",
|
||||
"tooltipPreviewFormatting": "Preview Message Formatting",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipItalicize": "Italic",
|
||||
"tooltipBackToMessageEditing": "Back to Message Editing",
|
||||
"tooltipBoldText": "Bold",
|
||||
"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",
|
||||
"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?",
|
||||
"clickableLinksCopy": "Copy URL",
|
||||
"clickableLinkOpen": "Open URL",
|
||||
"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",
|
||||
"conversationNotificationPolicySettingDescription": "Control notification behaviour for this conversation",
|
||||
"localeRo": "Romanian",
|
||||
"newMessageNotificationConversationInfo": "New Message From %1",
|
||||
"newMessageNotificationSimple": "New Message",
|
||||
"notificationContentContactInfo": "Conversation Information",
|
||||
|
@ -62,7 +116,6 @@
|
|||
"themeNameWitch": "Witch",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingDownloadFolder": "Download Folder",
|
||||
"settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.",
|
||||
"settingImagePreviews": "Image Previews and Profile Pictures",
|
||||
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
|
||||
"enableExperimentClickableLinks": "Enable Clickable Links",
|
||||
|
@ -80,7 +133,6 @@
|
|||
"importLocalServerLabel": "Import a locally hosted server",
|
||||
"savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
|
@ -152,7 +204,6 @@
|
|||
"settingUIColumnSingle": "Single",
|
||||
"settingUIColumnLandscape": "UI Columns in Landscape Mode",
|
||||
"settingUIColumnPortrait": "UI Columns in Portrait Mode",
|
||||
"localePl": "Polish",
|
||||
"tooltipRemoveThisQuotedMessage": "Remove quoted message.",
|
||||
"tooltipReplyToThisMessage": "Reply to this message",
|
||||
"tooltipRejectContactRequest": "Reject this contact request",
|
||||
|
@ -209,8 +260,6 @@
|
|||
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
|
||||
"enableGroups": "Enable Group Chat",
|
||||
"experimentsEnabled": "Enable Experiments",
|
||||
"localeIt": "Italiana",
|
||||
"localeEs": "Espanol",
|
||||
"addListItem": "Add a New List Item",
|
||||
"addNewItem": "Add a new item to the list",
|
||||
"todoPlaceholder": "Todo...",
|
||||
|
@ -229,10 +278,6 @@
|
|||
"themeLight": "Light",
|
||||
"largeTextLabel": "Large",
|
||||
"settingInterfaceZoom": "Zoom level",
|
||||
"localeDe": "Deutsche",
|
||||
"localePt": "Portuguesa",
|
||||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Language",
|
||||
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
|
||||
"versionBuilddate": "Version: %1 Built on: %2",
|
||||
|
|
|
@ -1,7 +1,68 @@
|
|||
{
|
||||
"@@locale": "es",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"localeDa": "Danés",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"tooltipPinConversation": "Fija la conversación en la parte superior de \"Conversaciones\"",
|
||||
"errorDownloadDirectoryDoesNotExist": "No se puede habilitar el uso compartido de archivos porque la carpeta de descarga no se ha configurado o se configuró en una carpeta que no existe.",
|
||||
"acquiringTicketsFromServer": "Realizando Desafío Antispam",
|
||||
"acquiredTicketsFromServer": "Desafío Antispam Completado",
|
||||
"tooltipUnpinConversation": "Desenganchar la conversación de la parte superior de \"Conversaciones\"",
|
||||
"manageSharedFiles": "Administrar Archivos Compartidos",
|
||||
"stopSharingFile": "Dejar de compartir archivos",
|
||||
"restartFileShare": "Comenzar a compartir archivos",
|
||||
"viewReplies": "Ver respuestas a este mensaje",
|
||||
"headingReplies": "Respuestas",
|
||||
"messageNoReplies": "No hay respuestas a este mensaje.",
|
||||
"fileDownloadUnavailable": "Parece que este archivo no está disponible para descargar. Es posible que el remitente haya desactivado las descargas de este archivo.",
|
||||
"replyingTo": "Respondiendo a %1",
|
||||
"localeTr": "Turco \/ Türk",
|
||||
"localeIt": "Italiano \/ Italiano",
|
||||
"localeDe": "Alemán \/ Deutsch",
|
||||
"settingImagePreviewsDescription": "Las imágenes se descargarán y visualizarán automáticamente. Ten en cuenta que las previsualizaciones pueden generar vulnerabilidades de seguridad, no deberías habilitar este experimento si usas Cwtch con contactos que no son de confianza. Las imágenes de perfil están planeadas para Cwtch 1.6.",
|
||||
"tooltipBackToMessageEditing": "Volver a Edición de mensajes",
|
||||
"tooltipSubscript": "Subíndice",
|
||||
"tooltipPreviewFormatting": "Vista previa del formato del mensaje",
|
||||
"tooltipBoldText": "Negrita",
|
||||
"tooltipItalicize": "Itálico",
|
||||
"tooltipSuperscript": "Superíndice",
|
||||
"tooltipCode": "Código \/ Monoespaciado",
|
||||
"exportProfileTooltip": "Haga una copia de seguridad de este perfil en un archivo cifrado. El archivo cifrado puede ser importado en otra aplicación Cwtch.",
|
||||
"tooltipStrikethrough": "Tachado",
|
||||
"settingAndroidPowerExemption": "Android ignora las optimizaciones de batería",
|
||||
"messageFormattingDescription": "Habilitar el formato de texto enriquecido en los mensajes mostrados, por ejemplo, **negrita** y *cursiva*.",
|
||||
"clickableLinksWarning": "Abrir esta URL lanzará una aplicación fuera de Cwtch que puede revelar metadatos o comprometer la seguridad de Cwtch. Solo abra URLs de personas confiables. ¿Desea continuar?",
|
||||
"settingsAndroidPowerReenablePopup": "No se puede habilitar la optimización desde Cwtch. Vaya a Android \/ Configuración \/ Aplicaciones \/ Cwtch \/ Batería y configure Uso a \"Optimizado\"",
|
||||
"settingAndroidPowerExemptionDescription": "Opcional: Solicite a Android eximir a Cwtch de la administración de energía optimizada. Esto resultará en mejor estabilidad a costa de mayor uso de batería.",
|
||||
"successfullyImportedProfile": "Perfil importado correctamente:",
|
||||
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Esta función requiere que el experimento de grupos esté habilitado en Configuración",
|
||||
"clickableLinkError": "Se encontró un error al abrir la URL",
|
||||
"importProfileTooltip": "Usar una copia de seguridad cifrada de Cwtch para incorporar un perfil creado en otra instancia de Cwtch.",
|
||||
"chatHistoryDefault": "Esta conversación será eliminada cuando se cierre Cwtch! El historial de mensajes puede habilitarse en cada conversación usando el menú de Configuración en la parte superior derecha.",
|
||||
"torSettingsEnabledCacheDescription": "Almacena en caché el consenso de Tor actual para re usar la próxima vez que Cwtch se abra. Esto permitirá que Tor inicie más rápido. Cuando se desactiva, Cwtch eliminará los datos almacenados en caché al iniciarse.",
|
||||
"exportProfile": "Exportar Perfil",
|
||||
"importProfile": "Importar Perfil",
|
||||
"failedToImportProfile": "Error Importando Perfil",
|
||||
"shuttingDownApp": "Cerrando...",
|
||||
"clickableLinkOpen": "Abrir URL",
|
||||
"clickableLinksCopy": "Copiar URL",
|
||||
"formattingExperiment": "Formato de Mensajes",
|
||||
"okButton": "OK",
|
||||
"localeFr": "Francés \/ Français",
|
||||
"localePt": "Portugués \/ Portuguesa",
|
||||
"localeEs": "Español \/ Español",
|
||||
"localeRU": "Ruso \/ Русский",
|
||||
"localePl": "Polaco \/ Polski",
|
||||
"localeLb": "Luxemburgués \/ Lëtzebuergesch",
|
||||
"localeEn": "Inglés \/ English",
|
||||
"localeDa": "Danés \/ Dansk",
|
||||
"localeCy": "Galés \/ Cymraeg",
|
||||
"localeNo": "Noruego \/ Norsk",
|
||||
"localeEl": "Griego \/ Ελληνικά",
|
||||
"localeRo": "Rumano \/ Română",
|
||||
"groupInviteSettingsWarning": "¡Has recibido una invitación para unirte a un grupo! Por favor habilita el experimento de chat grupal en Configuración para ver esta invitación",
|
||||
"plainServerDescription": "Te recomendamos que protejas tus servidores de Cwtch con una contraseña. Si no estableces una contraseña en este servidor, cualquiera que tenga acceso a este dispositivo podrá acceder a la información sobre este servidor incluyendo claves criptográficas confidenciales",
|
||||
"encryptedServerDescription": "Cifrar un servidor con una contraseña lo protege de otras personas que puedan usar este dispositivo. Los servidores cifrados no se pueden descifrar, mostrar ni acceder hasta que se ingrese la contraseña correcta para desbloquearlos",
|
||||
|
@ -14,9 +75,7 @@
|
|||
"cancel": "Cancelar",
|
||||
"torStatus": "Estado de Tor",
|
||||
"torVersion": "Versión de Tor",
|
||||
"settingImagePreviewsDescription": "Las imágenes se descargarán y visualizarán automáticamente. Ten en cuenta que las previsualizaciones pueden generar vulnerabilidades de seguridad, no deberías habilitar este experimento si usas Cwtch con contactos que no son de confianza. Las imágenes de perfil están planeadas para Cwtch 1.6.",
|
||||
"descriptionStreamerMode": "Si está activada, esta opción hace que la aplicación sea más privada visualmente para transmitir o presentar, por ejemplo, ocultando direcciones de perfil y contacto.",
|
||||
"localeCy": "Galés",
|
||||
"blockUnknownConnectionsEnabledDescription": "Las conexiones de contactos desconocidos están bloqueadas. Puedes cambiar esto en la Configuración",
|
||||
"streamerModeLabel": "Modo Streamer\/Presentación",
|
||||
"deleteProfileSuccess": "Perfil eliminado correctamente",
|
||||
|
@ -87,7 +146,6 @@
|
|||
"fileCheckingStatus": "Comprobación del estado de la descarga",
|
||||
"verfiyResumeButton": "Verificar\/reanudar",
|
||||
"copyServerKeys": "Copiar claves",
|
||||
"localeRU": "Ruso",
|
||||
"newMessagesLabel": "Nuevos Mensajes",
|
||||
"importLocalServerLabel": "Importar un servidor alojado localmente",
|
||||
"importLocalServerSelectText": "Seleccionar el servidor local",
|
||||
|
@ -116,15 +174,10 @@
|
|||
"descriptionACNCircuitInfo": "Información detallada sobre la ruta que la red de comunicación anónima está utilizando para conectarse a esta conversación.",
|
||||
"torSettingsCustomSocksPort": "Puerto SOCKS personalizado",
|
||||
"labelTorNetwork": "Red Tor",
|
||||
"torSettingsEnabledCacheDescription": "Almacena en caché el consenso de Tor actual para reusar la próxima vez que Cwtch se abra. Esto permitirá que Tor inicie más rápido. Cuando se desactiva, Cwtch eliminará los datos almacenados en caché al iniciarse.",
|
||||
"notificationPolicyMute": "Silenciar",
|
||||
"notificationContentContactInfo": "Información de la conversación",
|
||||
"newMessageNotificationSimple": "Nuevo Mensaje",
|
||||
"newMessageNotificationConversationInfo": "Nuevo mensaje de %1",
|
||||
"localeRo": "Rumano",
|
||||
"localeLb": "Luxemburgués",
|
||||
"localeNo": "Noruego",
|
||||
"localeEl": "Griego",
|
||||
"notificationPolicyDefaultAll": "Todas predeterminadas",
|
||||
"notificationContentSimpleEvent": "Evento simple",
|
||||
"conversationNotificationPolicyDefault": "Predeterminado",
|
||||
|
@ -149,7 +202,6 @@
|
|||
"settingUIColumnSingle": "Sencillo",
|
||||
"settingUIColumnLandscape": "Columnas de la interfaz en modo horizontal",
|
||||
"settingUIColumnPortrait": "Columnas de la interfaz en modo vertical",
|
||||
"localePl": "Polaco",
|
||||
"tooltipRemoveThisQuotedMessage": "Remover el mensaje citado",
|
||||
"tooltipReplyToThisMessage": "Responder a este mensaje",
|
||||
"tooltipRejectContactRequest": "Rechazar esta solicitud de contacto",
|
||||
|
@ -171,7 +223,6 @@
|
|||
"contactSuggestion": "Esta es una sugerencia de contacto para:",
|
||||
"rejected": "¡Rechazado!",
|
||||
"accepted": "¡Aceptado!",
|
||||
"chatHistoryDefault": "Esta conversación será eliminada cuando se cierre Cwtch! El historial de mensajes puede habilitarse en cada conversación usando el menú de Configuración en la parte superior derecha. ",
|
||||
"yesLeave": "Si, Salir de Esta Conversación",
|
||||
"newPassword": "Nueva Contraseña",
|
||||
"reallyLeaveThisGroupPrompt": "¿Estás seguro de que quieres salir de esta conversación?",
|
||||
|
@ -221,9 +272,6 @@
|
|||
"themeDark": "Oscuro",
|
||||
"smallTextLabel": "Pequeño",
|
||||
"builddate": "Basado en: %2",
|
||||
"localeDe": "Alemán",
|
||||
"localePt": "Portugués",
|
||||
"localeFr": "Francés",
|
||||
"addListItem": "Añadir un nuevo elemento a la lista",
|
||||
"joinGroupTab": "Únete a un grupo",
|
||||
"viewGroupMembershipTooltip": "Ver membresía del grupo",
|
||||
|
@ -232,14 +280,12 @@
|
|||
"chatBtn": "Chat",
|
||||
"password": "Contraseña",
|
||||
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
|
||||
"localeIt": "Italiano",
|
||||
"acknowledgedLabel": "Reconocido",
|
||||
"defaultProfileName": "Alicia",
|
||||
"versionBuilddate": "Versión: %1 Basado en %2",
|
||||
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)",
|
||||
"themeLight": "Claro",
|
||||
"versionTor": "Versión %1 con tor %2",
|
||||
"localeEs": "Español",
|
||||
"networkStatusOnline": "En línea",
|
||||
"newConnectionPaneTitle": "Nueva conexión",
|
||||
"addNewItem": "Añadir un nuevo elemento a la lista",
|
||||
|
@ -309,7 +355,6 @@
|
|||
"unlock": "Desbloquear",
|
||||
"cwtchSettingsTitle": "Configuración de Cwtch",
|
||||
"settingLanguage": "Idioma",
|
||||
"localeEn": "Inglés",
|
||||
"settingInterfaceZoom": "Nivel de zoom",
|
||||
"largeTextLabel": "Grande",
|
||||
"version": "Versión %1",
|
||||
|
|
|
@ -1,15 +1,80 @@
|
|||
{
|
||||
"@@locale": "fr",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"localeDa": "Danois",
|
||||
"localeCy": "Gallois",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Le partage de fichiers ne peut pas être activé car le dossier de téléchargement n'a pas été défini ou est défini sur un dossier qui n'existe pas.",
|
||||
"localeIt": "italien \/ italien",
|
||||
"localeTr": "Turc \/ Türk",
|
||||
"tooltipPinConversation": "Épingler la conversation en haut de «Conversations»",
|
||||
"tooltipUnpinConversation": "Détacher la conversation du haut de «Conversations»",
|
||||
"viewReplies": "Voir les réponses à ce message",
|
||||
"stopSharingFile": "Arrêter le partage de fichiers",
|
||||
"restartFileShare": "Démarrer le partage de fichiers",
|
||||
"replyingTo": "Répondre à %1",
|
||||
"messageNoReplies": "Il n'y a pas de réponses à ce message.",
|
||||
"manageSharedFiles": "Gérer les fichiers partagés",
|
||||
"headingReplies": "Réponses",
|
||||
"fileDownloadUnavailable": "Ce fichier semble indisponible pour le téléchargement. L'expéditeur a peut-être désactivé les téléchargements pour ce fichier.",
|
||||
"localeDe": "Allemand \/ Deutsch",
|
||||
"localeDa": "Danois \/ Dansk",
|
||||
"localePt": "Portugais \/ Portuguesa",
|
||||
"localeCy": "Gallois \/ Cymraeg",
|
||||
"localeEl": "Grec \/ Ελληνικά",
|
||||
"localeRU": "Russe \/ Русский",
|
||||
"localeLb": "Luxembourgeois \/ Lëtzebuergesch",
|
||||
"localePl": "Polonais \/ Polski",
|
||||
"localeEs": "Espagnol \/ Español",
|
||||
"localeRo": "Roumain \/ Română",
|
||||
"localeFr": "Français \/ Français",
|
||||
"localeNo": "Norvégien \/ Norsk",
|
||||
"localeEn": "Anglais \/ English",
|
||||
"settingImagePreviewsDescription": "Les images et les photos de profil seront téléchargées et prévisualisées automatiquement. Nous vous recommandons de ne pas activer cette expérience si vous utilisez Cwtch avec des contacts non fiables.",
|
||||
"tooltipSuperscript": "Exposant",
|
||||
"tooltipSubscript": "Indice",
|
||||
"tooltipStrikethrough": "Barré",
|
||||
"tooltipPreviewFormatting": "Aperçu de la mise en forme du message",
|
||||
"tooltipItalicize": "Italique",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipBoldText": "Gras",
|
||||
"tooltipBackToMessageEditing": "Retour à l'édition des messages",
|
||||
"acceptGroupBtn": "Accepter",
|
||||
"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 l’expé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",
|
||||
"clickableLinkError": "Erreur rencontrée lors de la tentative d'ouverture de l'URL",
|
||||
"successfullyImportedProfile": "Profil importé avec succès : %profile",
|
||||
"shuttingDownApp": "Fermeture...",
|
||||
"importProfileTooltip": "Utilisez une sauvegarde Cwtch chiffrée pour importer un profil créé dans une autre instance de Cwtch.",
|
||||
"exportProfile": "Exporter un profile",
|
||||
"importProfile": "Importer un profil",
|
||||
"failedToImportProfile": "Erreur lors de l'importation du profil",
|
||||
"exportProfileTooltip": "Sauvegardez ce profil dans un fichier chiffré. Le fichier crypté peut être importé dans une autre application Cwtch.",
|
||||
"contactSuggestion": "Il s'agit d'une suggestion de contact pour:",
|
||||
"contactGoto": "Aller à la conversation avec %1",
|
||||
"yourDisplayName": "Votre nom d'usage",
|
||||
"profileName": "Nom d'usage",
|
||||
"displayNameLabel": "Nom d'usage",
|
||||
"displayNameTooltip": "Veuillez entrer un nom d'usage s'il vous plaît",
|
||||
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe de",
|
||||
"encryptedProfileDescription": "Le chiffrement d'un profil à l'aide d'un mot de passe le protège des autres personnes susceptibles d'utiliser également cet appareil. Les profils chiffrés ne peuvent pas être déchiffrés, affichés ou accessibles tant que le mot de passe correct n'a pas été saisi pour les déverrouiller.",
|
||||
"notificationContentSimpleEvent": "Événement ordinaire",
|
||||
"placeholderEnterMessage": "Saisissez un message",
|
||||
"conversationNotificationPolicySettingDescription": "Contrôler le comportement de notification de cette conversation",
|
||||
"localeEl": "Grec",
|
||||
"localeNo": "Norvégien",
|
||||
"localeLb": "Luxembourgeois",
|
||||
"notificationPolicySettingDescription": "Contrôle le comportement de notification d'application par défaut",
|
||||
"notificationContentSettingDescription": "Contrôle le contenu des notifications de conversation",
|
||||
"notificationContentSimpleEvent": "Plain-Evénement",
|
||||
"conversationNotificationPolicySettingLabel": "Politique de notification des conversations",
|
||||
"notificationContentContactInfo": "Informations sur les conversations",
|
||||
"notificationContentSettingLabel": "Contenu des notifications",
|
||||
|
@ -26,7 +91,6 @@
|
|||
"settingsGroupExperiments": "Expériences",
|
||||
"newMessageNotificationConversationInfo": "Nouveau message d'%1",
|
||||
"newMessageNotificationSimple": "Nouveau message",
|
||||
"localeRo": "Roumain",
|
||||
"editProfile": "Modifier le profil",
|
||||
"settingTheme": "Utilisez des thèmes clairs",
|
||||
"torSettingsUseCustomTorServiceConfiguration": "Utiliser une configuration personnalisée du service Tor (torrc)",
|
||||
|
@ -46,7 +110,6 @@
|
|||
"fileSharingSettingsDownloadFolderDescription": "Lorsque les fichiers sont téléchargés automatiquement (par exemple, les fichiers image, lorsque les aperçus d'image sont activés), un emplacement par défaut pour télécharger les fichiers est nécessaire.",
|
||||
"descriptionACNCircuitInfo": "Informations détaillées sur le chemin que le réseau de communication anonyme utilise pour se connecter à cette conversation.",
|
||||
"msgConfirmSend": "Êtes-vous sûr de vouloir envoyer",
|
||||
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe de",
|
||||
"msgFileTooBig": "La taille du fichier ne peut pas dépasser 10 Go",
|
||||
"msgAddToAccept": "Ajoutez ce compte à vos contacts afin d'accepter ce fichier.",
|
||||
"btnSendFile": "Envoyer le fichier",
|
||||
|
@ -63,7 +126,6 @@
|
|||
"themeNameGhost": "Fantôme",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"themeColorLabel": "Thème de couleur",
|
||||
"settingImagePreviewsDescription": "Les images seront téléchargées et prévisualisées automatiquement. Veuillez noter que la prévisualisation des images peut souvent conduire à des failles de sécurité, et vous ne devriez pas activer cette expérience si vous utilisez Cwtch avec des contacts non fiables. Les images de profil sont prévues pour Cwtch 1.6.",
|
||||
"settingImagePreviews": "Aperçu des images et photos de profil",
|
||||
"settingDownloadFolder": "Dossier de téléchargement",
|
||||
"enableExperimentClickableLinks": "Activer les liens cliquables",
|
||||
|
@ -78,10 +140,8 @@
|
|||
"importLocalServerButton": "Importer %1",
|
||||
"groupsOnThisServerLabel": "Les groupes dont je fais partie sont hébergés sur ce serveur",
|
||||
"fieldDescriptionLabel": "Description",
|
||||
"displayNameTooltip": "Veuillez entrer un nom d'usage s'il vous plaît",
|
||||
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au contact.",
|
||||
"newMessagesLabel": "Nouveaux messages",
|
||||
"localeRU": "Russe",
|
||||
"copyServerKeys": "Copier les clés",
|
||||
"verfiyResumeButton": "Vérifier\/reprendre",
|
||||
"fileSavedTo": "Enregistré dans",
|
||||
|
@ -141,11 +201,8 @@
|
|||
"archiveConversation": "Archiver cette conversation",
|
||||
"blockedMessageMessage": "Ce message provient d'un profil que vous avez bloqué.",
|
||||
"showMessageButton": "Afficher le message",
|
||||
"placeholderEnterMessage": "saisissez un message",
|
||||
"encryptedProfileDescription": "Le chiffrement d'un profil à l'aide d'un mot de passe le protège des autres personnes susceptibles d'utiliser également cet appareil. Les profils chiffrés ne peuvent pas être déchiffrés , affichés ou accessibles tant que le mot de passe correct n'a pas été saisi pour les déverrouiller.",
|
||||
"plainProfileDescription": "Nous vous recommandons de protéger vos profils Cwtch par un mot de passe. Si vous ne définissez pas de mot de passe sur ce profil, toute personne ayant accès à cet appareil peut être en mesure d'accéder aux informations relatives à ce profil, y compris les contacts, les messages et les clés de chiffrement sensibles.",
|
||||
"addContactConfirm": "Ajouter le contact %1",
|
||||
"contactGoto": "Aller à la conversation avec %1",
|
||||
"addContact": "Ajouter le contact",
|
||||
"settingUIColumnOptionSame": "Même réglage que pour le mode portrait",
|
||||
"settingUIColumnDouble14Ratio": "Double (1:4)",
|
||||
|
@ -153,7 +210,6 @@
|
|||
"settingUIColumnSingle": "Simple",
|
||||
"settingUIColumnLandscape": "Colonnes de l'interface utilisateur en mode paysage",
|
||||
"settingUIColumnPortrait": "Colonnes de l'interface utilisateur en mode portrait",
|
||||
"localePl": "Polonais",
|
||||
"tooltipReplyToThisMessage": "Répondre à ce message",
|
||||
"tooltipRemoveThisQuotedMessage": "Supprimer le message cité.",
|
||||
"deleteProfileConfirmBtn": "Supprimer vraiment le profil ?",
|
||||
|
@ -165,7 +221,6 @@
|
|||
"deleteBtn": "Effacer",
|
||||
"acknowledgedLabel": "Accusé de réception",
|
||||
"zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)",
|
||||
"localeIt": "Italien",
|
||||
"versionTor": "Version %1 avec tor %2",
|
||||
"version": "Version %1",
|
||||
"builddate": "Construit le : %2",
|
||||
|
@ -173,7 +228,6 @@
|
|||
"tooltipAcceptContactRequest": "Acceptez cette demande de contact.",
|
||||
"tooltipRejectContactRequest": "Refuser cette demande de contact",
|
||||
"addNewItem": "Ajouter un nouvel élément à la liste",
|
||||
"localeEs": "Espagnol",
|
||||
"todoPlaceholder": "À faire...",
|
||||
"pasteAddressToAddContact": "Collez une adresse cwtch, une invitation ou un ensemble de clés ici pour ajouter une nouvelle conversation",
|
||||
"addListItem": "Ajouter un nouvel élément de liste",
|
||||
|
@ -200,7 +254,6 @@
|
|||
"addListItemBtn": "Ajouter un élément",
|
||||
"addProfileTitle": "Ajouter un nouveau profil",
|
||||
"editProfileTitle": "Modifier le profil",
|
||||
"profileName": "Pseudo",
|
||||
"defaultProfileName": "Alice",
|
||||
"newProfile": "Nouveau profil",
|
||||
"deleteConfirmText": "SUPPRIMER",
|
||||
|
@ -219,10 +272,6 @@
|
|||
"yourServers": "Vos serveurs",
|
||||
"unlock": "Déverrouiller",
|
||||
"settingLanguage": "Langue",
|
||||
"localeEn": "Anglais",
|
||||
"localeFr": "Français",
|
||||
"localePt": "Portugais",
|
||||
"localeDe": "Allemand",
|
||||
"settingInterfaceZoom": "Niveau de zoom",
|
||||
"themeLight": "Clair",
|
||||
"themeDark": "Sombre",
|
||||
|
@ -233,7 +282,6 @@
|
|||
"reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.",
|
||||
"yesLeave": "Oui, quittez cette conversation",
|
||||
"noPasswordWarning": "Ne pas utiliser de mot de passe sur ce compte signifie que toutes les données stockées localement ne seront pas chiffrées.",
|
||||
"yourDisplayName": "Pseudo",
|
||||
"currentPasswordLabel": "Mot de passe actuel",
|
||||
"password1Label": "Mot de passe",
|
||||
"password2Label": "Saisissez à nouveau le mot de passe",
|
||||
|
@ -263,7 +311,6 @@
|
|||
"chatHistoryDefault": "Cette conversation sera supprimée lorsque Cwtch sera fermé ! L'historique des messages peut être activé pour la conversation via le menu Paramètres en haut à droite.",
|
||||
"accepted": "Accepté !",
|
||||
"rejected": "Rejeté !",
|
||||
"contactSuggestion": "Il s'agit d'une suggestion de contact pour : ",
|
||||
"sendAnInvitation": "Vous avez envoyé une invitation pour : ",
|
||||
"torVersion": "Version de Tor",
|
||||
"torStatus": "Statut de Tor",
|
||||
|
@ -291,7 +338,6 @@
|
|||
"largeTextLabel": "Large",
|
||||
"cwtchSettingsTitle": "Préférences Cwtch",
|
||||
"saveBtn": "Sauvegarder",
|
||||
"displayNameLabel": "Pseudo",
|
||||
"copiedToClipboardNotification": "Copié dans le presse-papier",
|
||||
"addressLabel": "Adresse",
|
||||
"puzzleGameBtn": "Puzzle",
|
||||
|
@ -299,7 +345,6 @@
|
|||
"listsBtn": "Listes",
|
||||
"chatBtn": "Discuter",
|
||||
"rejectGroupBtn": "Refuser",
|
||||
"acceptGroupBtn": "Accepter",
|
||||
"newGroupBtn": "Créer un nouveau groupe",
|
||||
"copyBtn": "Copier",
|
||||
"pendingLabel": "En attente",
|
||||
|
|
|
@ -1,8 +1,80 @@
|
|||
{
|
||||
"@@locale": "it",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"localeDa": "Danese",
|
||||
"localeCy": "Gallese",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"tooltipPinConversation": "Aggiungi la conversazione in cima alla lista \"Conversazioni\"",
|
||||
"tooltipUnpinConversation": "Rimuovi la conversazione dalla cima della lista \"Conversazioni\"",
|
||||
"errorDownloadDirectoryDoesNotExist": "La condivisione file non può essere abilitata perché la destinazione dei download non è stata impostata o è impostata su una cartella che non esiste.",
|
||||
"acquiringTicketsFromServer": "Esecuzione dell'operazione antispam",
|
||||
"acquiredTicketsFromServer": "Operazione antispam completata",
|
||||
"shareProfileMenuTooltop": "Condividi il profilo tramite...",
|
||||
"shareMenuQRCode": "Mostra il codice QR",
|
||||
"enableExperimentQRCode": "Codici QR",
|
||||
"experimentQRCodeDescription": "Il supporto del codice QR consente la condivisione di dati (come l'identità del profilo) tramite codici QR",
|
||||
"localeTr": "Turco \/ Türk",
|
||||
"localeIt": "Italiano \/ Italiano",
|
||||
"fileDownloadUnavailable": "Questo file non sembra disponibile per il download. Il mittente potrebbe aver disabilitato i download per questo file.",
|
||||
"headingReplies": "Risposte",
|
||||
"manageSharedFiles": "Gestisci file condivisi",
|
||||
"messageNoReplies": "Non ci sono risposte a questo messaggio.",
|
||||
"replyingTo": "Risposta a %1",
|
||||
"restartFileShare": "Avvia la condivisione del file",
|
||||
"stopSharingFile": "Interrompi la condivisione del file",
|
||||
"viewReplies": "Visualizza le risposte a questo messaggio",
|
||||
"localeDe": "Tedesco \/ Deutsch",
|
||||
"settingImagePreviewsDescription": "Le immagini e le immagini del profilo verranno scaricate e visualizzate in anteprima automaticamente. Ti consigliamo di non abilitare questo esperimento se usi Cwtch con contatti non attendibili.",
|
||||
"localeNo": "Norvegese \/ Norsk",
|
||||
"localeCy": "Gallese \/ Cymraeg",
|
||||
"localeFr": "Francese \/ Français",
|
||||
"localePl": "Polacco \/ Polski",
|
||||
"localePt": "Portoghese \/ Portuguesa",
|
||||
"localeDa": "Danese \/ Dansk",
|
||||
"localeEn": "Inglese \/ English",
|
||||
"localeRo": "Rumeno \/ Română",
|
||||
"localeEl": "Greco \/ Ελληνικά",
|
||||
"localeLb": "Lussemburghese \/ Lëtzebuergesch",
|
||||
"localeEs": "Spagnolo \/ Español",
|
||||
"localeRU": "Russo \/ Русский",
|
||||
"tooltipCode": "Codice \/ Monospazio",
|
||||
"tooltipPreviewFormatting": "Anteprima della formattazione del messaggio",
|
||||
"tooltipStrikethrough": "Barrato",
|
||||
"tooltipSubscript": "Pedice",
|
||||
"tooltipSuperscript": "Apice",
|
||||
"tooltipItalicize": "Corsivo",
|
||||
"tooltipBackToMessageEditing": "Torna a Modifica del messaggio",
|
||||
"tooltipBoldText": "Grassetto",
|
||||
"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",
|
||||
"settingTheme": "Usa Temi Leggeri",
|
||||
"editProfile": "Modifica il profilo",
|
||||
"labelTorNetwork": "Rete tor",
|
||||
|
@ -49,10 +121,6 @@
|
|||
"settingsGroupExperiments": "Esperimenti",
|
||||
"conversationNotificationPolicySettingLabel": "Criteri di notifica delle conversazioni",
|
||||
"conversationNotificationPolicySettingDescription": "Controlla il comportamento delle notifiche per questa conversazione",
|
||||
"localeRo": "Rumeno",
|
||||
"localeLb": "Lussemburghese",
|
||||
"localeNo": "Norvegese",
|
||||
"localeEl": "Greco",
|
||||
"serverNotSynced": "Sincronizzazione Nuovi Messaggi (L'operazione può richiedere del tempo)...",
|
||||
"blockUnknownLabel": "Blocca Contatti Sconosciuti",
|
||||
"addPeer": "Aggiungi Contatto",
|
||||
|
@ -67,10 +135,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",
|
||||
|
@ -84,22 +148,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",
|
||||
|
@ -129,7 +183,6 @@
|
|||
"serverConnectionsLabel": "Connessione",
|
||||
"experimentClickableLinksDescription": "L'esperimento dei link cliccabili permette di cliccare sugli URL condivisi nei messaggi",
|
||||
"settingImagePreviews": "Anteprime delle immagini e immagini del profilo",
|
||||
"settingImagePreviewsDescription": "Le immagini verranno scaricate e visualizzate in anteprima automaticamente. Tieni presente che le anteprime delle immagini possono spesso portare a vulnerabilità di sicurezza e non dovresti abilitare questo esperimento se usi Cwtch con contatti non attendibili. Le immagini del profilo sono previste per Cwtch 1.6.",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"themeNameWitch": "Strega",
|
||||
"themeNameVampire": "Vampiro",
|
||||
|
@ -154,7 +207,6 @@
|
|||
"titleManageProfilesShort": "Profili",
|
||||
"tooltipReplyToThisMessage": "Rispondi a questo messaggio",
|
||||
"tooltipRemoveThisQuotedMessage": "Rimuovi il messaggio citato.",
|
||||
"localePl": "Polacco",
|
||||
"settingUIColumnPortrait": "Colonne dell'interfaccia utente in modalità verticale",
|
||||
"settingUIColumnLandscape": "Colonne dell'interfaccia utente in modalità orizzontale",
|
||||
"settingUIColumnSingle": "Singola",
|
||||
|
@ -193,7 +245,6 @@
|
|||
"fileCheckingStatus": "Controllo dello stato del download",
|
||||
"verfiyResumeButton": "Verifica\/riprendi",
|
||||
"copyServerKeys": "Copia chiavi",
|
||||
"localeRU": "Russo",
|
||||
"messageFileOffered": "Il contatto offre l'invio di un file",
|
||||
"tooltipSendFile": "Invia file",
|
||||
"settingFileSharing": "Condivisione file",
|
||||
|
@ -238,12 +289,6 @@
|
|||
"versionTor": "Versione %1 con tor %2",
|
||||
"version": "Versione %1",
|
||||
"builddate": "Costruito il: %2",
|
||||
"localeEn": "Inglese",
|
||||
"localeIt": "Italiano",
|
||||
"localeEs": "Spagnolo",
|
||||
"localeDe": "Tedesco",
|
||||
"localePt": "Portoghese",
|
||||
"localeFr": "Francese",
|
||||
"serverLabel": "Server",
|
||||
"createGroupBtn": "Crea",
|
||||
"createGroupTab": "Crea un gruppo",
|
||||
|
|
|
@ -1,8 +1,144 @@
|
|||
{
|
||||
"@@locale": "lb",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"localeDa": "Danish",
|
||||
"localeCy": "Welsh",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Tierkesch \/ Türk",
|
||||
"localeIt": "Italienesch",
|
||||
"localeEn": "Englesch \/ English",
|
||||
"localeFr": "Franséisch \/ Français",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Däitsch \/ Deutsch",
|
||||
"localeLb": "Lëtzebuergesch",
|
||||
"localeNo": "Norwegesch",
|
||||
"localeEl": "Griichesch",
|
||||
"localePl": "Polish \/ Polski",
|
||||
"localeRo": "Rumänesch",
|
||||
"localeRU": "Russesch",
|
||||
"localeEs": "Spuenesch",
|
||||
"localeDa": "Dänesch",
|
||||
"localePt": "Portugisesch",
|
||||
"localeCy": "Waliser",
|
||||
"settingImagePreviewsDescription": "Images and Profile Pictures will be downloaded and previewed automatically. We recommend that you do not enable this Experiment if you use Cwtch with untrusted contacts.",
|
||||
"tooltipPreviewFormatting": "Preview Message Formatting",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipItalicize": "Italic",
|
||||
"tooltipBackToMessageEditing": "Back to Message Editing",
|
||||
"tooltipBoldText": "Bold",
|
||||
"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",
|
||||
"newPassword": "Neit Passwuert",
|
||||
"yesLeave": "Jo, Konversatioun verloossen",
|
||||
"leaveConversation": "Konversatioun verloossen",
|
||||
"titleManageServers": "Server managen",
|
||||
"titleManageProfiles": "Meng Profiller managen",
|
||||
"titleManageContacts": "Konversatiounen",
|
||||
"todoPlaceholder": "Todo...",
|
||||
"networkStatusOnline": "Online",
|
||||
"smallTextLabel": "Kleng",
|
||||
"builddate": "Opegbaut op : %2",
|
||||
"version": "Versioun %1",
|
||||
"versionTor": "Versioun %1 mad Tor %2",
|
||||
"settingInterfaceZoom": "Zoom Level",
|
||||
"largeTextLabel": "Grouss",
|
||||
"themeDark": "Donkel",
|
||||
"themeLight": "Hell",
|
||||
"settingLanguage": "Sprooch",
|
||||
"versionBuilddate": "Versioun: %1 Opgebaut op: %2",
|
||||
"cwtchSettingsTitle": "Cwtch Astellungen",
|
||||
"unlock": "Entsperren",
|
||||
"yourServers": "Meng Server",
|
||||
"addNewProfileBtn": "Neien Profil dobäifügen",
|
||||
"yourProfiles": "Meng Profiller",
|
||||
"password": "Passwuert",
|
||||
"deleteProfileConfirmBtn": "Wierklech d'Profil läschen",
|
||||
"deleteConfirmLabel": "Schreif LÄSCHEN fir ze confirméieren",
|
||||
"deleteConfirmText": "LÄSCHEN",
|
||||
"deleteProfileBtn": "Profil läschen",
|
||||
"saveProfileBtn": "Profil späicheren",
|
||||
"passwordErrorEmpty": "Passwuert kann net eidel sinn",
|
||||
"createProfileBtn": "Profil erstellen",
|
||||
"password2Label": "Passwuert nei aginn",
|
||||
"password1Label": "Passwuert",
|
||||
"currentPasswordLabel": "Aktuellt Passwuert",
|
||||
"radioNoPassword": "Onverschlësselt (keen Passwuert)",
|
||||
"radioUsePassword": "Passwuert",
|
||||
"newProfile": "Neien Profil",
|
||||
"editProfile": "Profil editéieren",
|
||||
"bulletinsBtn": "Bulletins",
|
||||
"acknowledgedLabel": "Unerkannt",
|
||||
"addListItemBtn": "Item dobäifügen",
|
||||
"addProfileTitle": "Neien Profil dobäifügen",
|
||||
"editProfileTitle": "Profil editéieren",
|
||||
"defaultProfileName": "Alice",
|
||||
"deleteBtn": "Läschen",
|
||||
"unblockBtn": "Kontakt entblockéieren",
|
||||
"dontSavePeerHistory": "Verlaf läschen",
|
||||
"savePeerHistory": "Verlaf späicheren",
|
||||
"blockBtn": "Kontakt blockéieren",
|
||||
"saveBtn": "Späicheren",
|
||||
"addressLabel": "Adress",
|
||||
"puzzleGameBtn": "Puzzle Spill",
|
||||
"listsBtn": "Leschten",
|
||||
"chatBtn": "Chat",
|
||||
"rejectGroupBtn": "Refuséieren",
|
||||
"acceptGroupBtn": "Acceptéieren",
|
||||
"newGroupBtn": "Neien Grupp erstellen",
|
||||
"copyBtn": "Kopéieren",
|
||||
"couldNotSendMsgError": "Dësen Message konnt net geschéckt ginn",
|
||||
"peerBlockedMessage": "Kontakt ass blockéiert",
|
||||
"dmTooltip": "Klicke fir ee Direkten Message",
|
||||
"peerNotOnline": "Kontakt ass offline. Applikatiounen kennen elo grad net genotzt ginn.",
|
||||
"searchList": "Lescht sichen",
|
||||
"update": "Update",
|
||||
"inviteToGroupLabel": "An d'Grupp alueden",
|
||||
"inviteBtn": "Alueden",
|
||||
"groupNameLabel": "Gruppennumm",
|
||||
"viewServerInfo": "Server Info",
|
||||
"settingDownloadFolder": "Download Dossier",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"themeNameWitch": "Witch",
|
||||
"themeNameGhost": "Ghost",
|
||||
"themeNameVampire": "Vampire",
|
||||
"themeNamePumpkin": "Pumpkin",
|
||||
"themeNameMermaid": "Mermaid",
|
||||
"themeNameMidnight": "Midnight",
|
||||
"themeNameNeon1": "Neon 1",
|
||||
"themeNameNeon2": "Neon 2",
|
||||
"serverSynced": "Synchroniséiert",
|
||||
"serverConnectivityDisconnected": "Server getrennt",
|
||||
"serverConnectivityConnected": "Server verbonnen",
|
||||
|
@ -62,10 +198,6 @@
|
|||
"notificationContentContactInfo": "Konversatiouns Informatiounen",
|
||||
"newMessageNotificationSimple": "Neie Message",
|
||||
"newMessageNotificationConversationInfo": "Neie Message vun %1",
|
||||
"localeRo": "Rumänesch",
|
||||
"localeLb": "Lëtzebuergesch",
|
||||
"localeNo": "Norwegesch",
|
||||
"localeEl": "Griichesch",
|
||||
"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",
|
||||
|
@ -77,17 +209,6 @@
|
|||
"storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...",
|
||||
"loadingCwtch": "Loading Cwtch...",
|
||||
"themeColorLabel": "Color Theme",
|
||||
"themeNameNeon2": "Neon2",
|
||||
"themeNameNeon1": "Neon1",
|
||||
"themeNameMidnight": "Midnight",
|
||||
"themeNameMermaid": "Mermaid",
|
||||
"themeNamePumpkin": "Pumpkin",
|
||||
"themeNameGhost": "Ghost",
|
||||
"themeNameVampire": "Vampire",
|
||||
"themeNameWitch": "Witch",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingDownloadFolder": "Download Folder",
|
||||
"settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.",
|
||||
"settingImagePreviews": "Image Previews and Profile Pictures",
|
||||
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
|
||||
"enableExperimentClickableLinks": "Enable Clickable Links",
|
||||
|
@ -104,7 +225,6 @@
|
|||
"importLocalServerSelectText": "Select Local Server",
|
||||
"importLocalServerLabel": "Import a locally hosted server",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
|
@ -164,7 +284,6 @@
|
|||
"settingUIColumnSingle": "Single",
|
||||
"settingUIColumnLandscape": "UI Columns in Landscape Mode",
|
||||
"settingUIColumnPortrait": "UI Columns in Portrait Mode",
|
||||
"localePl": "Polish",
|
||||
"tooltipRemoveThisQuotedMessage": "Remove quoted message.",
|
||||
"tooltipReplyToThisMessage": "Reply to this message",
|
||||
"tooltipRejectContactRequest": "Reject this contact request",
|
||||
|
@ -198,19 +317,13 @@
|
|||
"rejected": "Rejected!",
|
||||
"accepted": "Accepted!",
|
||||
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.",
|
||||
"newPassword": "New Password",
|
||||
"yesLeave": "Yes, Leave This Conversation",
|
||||
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
|
||||
"leaveConversation": "Leave This Conversation",
|
||||
"inviteToGroup": "You have been invited to join a group:",
|
||||
"titleManageServers": "Manage Servers",
|
||||
"successfullAddedContact": "Successfully added ",
|
||||
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
|
||||
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
|
||||
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
|
||||
"titleManageProfiles": "Manage Cwtch Profiles",
|
||||
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
|
||||
"titleManageContacts": "Conversations",
|
||||
"tooltipAddContact": "Add a new contact or conversation",
|
||||
"tooltipOpenSettings": "Open the settings pane",
|
||||
"contactAlreadyExists": "Contact Already Exists",
|
||||
|
@ -218,100 +331,32 @@
|
|||
"conversationSettings": "Conversation Settings",
|
||||
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
|
||||
"enableGroups": "Enable Group Chat",
|
||||
"localeIt": "Italiana",
|
||||
"localeEs": "Espanol",
|
||||
"todoPlaceholder": "Todo...",
|
||||
"addNewItem": "Add a new item to the list",
|
||||
"addListItem": "Add a New List Item",
|
||||
"newConnectionPaneTitle": "New Connection",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusConnecting": "Connecting to network and contacts...",
|
||||
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
|
||||
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
|
||||
"viewGroupMembershipTooltip": "View Group Membership",
|
||||
"loadingTor": "Loading tor...",
|
||||
"smallTextLabel": "Small",
|
||||
"defaultScalingText": "Default size text (scale factor:",
|
||||
"builddate": "Built on: %2",
|
||||
"version": "Version %1",
|
||||
"versionTor": "Version %1 with tor %2",
|
||||
"experimentsEnabled": "Enable Experiments",
|
||||
"themeDark": "Dark",
|
||||
"themeLight": "Light",
|
||||
"settingTheme": "Use Light Themes",
|
||||
"largeTextLabel": "Large",
|
||||
"settingInterfaceZoom": "Zoom level",
|
||||
"localeDe": "Deutsche",
|
||||
"localePt": "Portuguesa",
|
||||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Language",
|
||||
"blockUnknownLabel": "Block Unknown Contacts",
|
||||
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
|
||||
"versionBuilddate": "Version: %1 Built on: %2",
|
||||
"cwtchSettingsTitle": "Cwtch Settings",
|
||||
"unlock": "Unlock",
|
||||
"yourServers": "Your Servers",
|
||||
"yourProfiles": "Your Profiles",
|
||||
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
|
||||
"password": "Password",
|
||||
"enterProfilePassword": "Enter a password to view your profiles",
|
||||
"addNewProfileBtn": "Add new profile",
|
||||
"deleteConfirmText": "DELETE",
|
||||
"deleteProfileConfirmBtn": "Really Delete Profile",
|
||||
"deleteConfirmLabel": "Type DELETE to confirm",
|
||||
"deleteProfileBtn": "Delete Profile",
|
||||
"passwordChangeError": "Error changing password: Supplied password rejected",
|
||||
"passwordErrorMatch": "Passwords do not match",
|
||||
"saveProfileBtn": "Save Profile",
|
||||
"createProfileBtn": "Create Profile",
|
||||
"passwordErrorEmpty": "Password cannot be empty",
|
||||
"password2Label": "Reenter password",
|
||||
"password1Label": "Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"yourDisplayName": "Your Display Name",
|
||||
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
|
||||
"radioNoPassword": "Unencrypted (No password)",
|
||||
"radioUsePassword": "Password",
|
||||
"editProfile": "Edit Profile",
|
||||
"newProfile": "New Profile",
|
||||
"defaultProfileName": "Alice",
|
||||
"profileName": "Display name",
|
||||
"editProfileTitle": "Edit Profile",
|
||||
"addProfileTitle": "Add new profile",
|
||||
"deleteBtn": "Delete",
|
||||
"unblockBtn": "Unblock Contact",
|
||||
"dontSavePeerHistory": "Delete History",
|
||||
"savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.",
|
||||
"savePeerHistory": "Save History",
|
||||
"blockBtn": "Block Contact",
|
||||
"saveBtn": "Save",
|
||||
"displayNameLabel": "Display Name",
|
||||
"copiedToClipboardNotification": "Copied to Clipboard",
|
||||
"addressLabel": "Address",
|
||||
"puzzleGameBtn": "Puzzle Game",
|
||||
"bulletinsBtn": "Bulletins",
|
||||
"listsBtn": "Lists",
|
||||
"chatBtn": "Chat",
|
||||
"rejectGroupBtn": "Reject",
|
||||
"acceptGroupBtn": "Accept",
|
||||
"acceptGroupInviteLabel": "Do you want to accept the invitation to",
|
||||
"newGroupBtn": "Create new group",
|
||||
"copyBtn": "Copy",
|
||||
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
|
||||
"peerBlockedMessage": "Contact is blocked",
|
||||
"pendingLabel": "Pending",
|
||||
"acknowledgedLabel": "Acknowledged",
|
||||
"couldNotSendMsgError": "Could not send this message",
|
||||
"dmTooltip": "Click to DM",
|
||||
"membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.",
|
||||
"addListItemBtn": "Add Item",
|
||||
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
|
||||
"searchList": "Search List",
|
||||
"update": "Update",
|
||||
"inviteBtn": "Invite",
|
||||
"inviteToGroupLabel": "Invite to group",
|
||||
"groupNameLabel": "Group Name",
|
||||
"viewServerInfo": "Server Info",
|
||||
"serverNotSynced": "Syncing New Messages (This can take some time)..."
|
||||
}
|
|
@ -0,0 +1,362 @@
|
|||
{
|
||||
"@@locale": "nl",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"acknowledgedLabel": "Erkend",
|
||||
"descriptionFileSharing": "Het experiment met het delen van bestanden staat u toe om bestanden van Cwtch-contactpersonen en groepen te verzenden en te ontvangen. Merk op dat het delen van een bestand met een groep tot gevolg zal hebben dat leden van die groep direct via Cwtch verbinding met je maken om het bestand te downloaden.",
|
||||
"descriptionExperimentsGroups": "Het groepsexperiment staat Cwtch toe verbinding te maken met onvertrouwde serverinfrastructuur om communicatie met meer dan één contact te vergemakkelijken.",
|
||||
"notificationPolicyDefaultAll": "Standaard Alle",
|
||||
"conversationNotificationPolicyOptIn": "Opt-In",
|
||||
"acquiringTicketsFromServer": "Antispamuitdaging uitvoeren",
|
||||
"settingAndroidPowerExemption": "Android negeer batterij-optimalisaties",
|
||||
"experimentQRCodeDescription": "Ondersteuning voor QR-codes maakt het delen van gegevens (zoals profielidentiteit) via QR-codes mogelijk",
|
||||
"tooltipSelectACustomProfileImage": "Selecteer een zelf ingestelde profielafbeelding",
|
||||
"settingsAndroidPowerReenablePopup": "Batterijoptimalisatie kan niet opnieuw worden ingeschakeld vanuit Cwtch. Ga naar Android \/ Instellingen \/ Apps \/ Cwtch \/ Batterij en stel Gebruik in op 'Geoptimaliseerd'",
|
||||
"settingAndroidPowerExemptionDescription": "Optioneel: Vraag Android om geen geoptimaliseerd energiebeheer te gebruiken voor Cwtch. Dit zal resulteren in een betere stabiliteit ten koste van meer batterijgebruik.",
|
||||
"acquiredTicketsFromServer": "Antispamuitdaging voltooid",
|
||||
"descriptionExperiments": "Cwtch-experimenten zijn optionele, Opt-In functies die extra functionaliteit toevoegen aan Cwtch die andere privacyoverwegingen kunnen hebben dan traditionele 1: 1 metadata-resistente chat, bijvoorbeeld groepschat, botintegratie enz.",
|
||||
"plainProfileDescription": "We raden aan je Cwtch-profielen te beveiligen met een wachtwoord. Als je geen wachtwoord instelt op dit profiel heeft iedereen die toegang heeft tot dit apparaat mogelijk toegang tot informatie over dit profiel, inclusief gevoelige cryptografische sleutels.",
|
||||
"plainServerDescription": "We raden aan je Cwtch-servers te beveiligen met een wachtwoord. Als je geen wachtwoord instelt op deze server heeft iedereen die toegang heeft tot dit apparaat mogelijk toegang tot informatie over deze server, inclusief gevoelige cryptografische sleutels.",
|
||||
"descriptionStreamerMode": "Wanneer ingeschakeld, maakt deze optie de app visueel meer privé voor streaming of presentatie door bijvoorbeeld profiel- en contactadressen te verbergen",
|
||||
"encryptedProfileDescription": "Het versleutelen van een profiel met een wachtwoord beschermt deze tegen andere mensen die dit apparaat kunnen gebruiken. Versleutelde profielen kunnen niet worden ontsleuteld, weergegeven of geopend totdat het juiste wachtwoord is ingevoerd om ze te ontgrendelen.",
|
||||
"groupInviteSettingsWarning": "Je bent uitgenodigd om lid te worden van een groep! Schakel het Groepschat-experiment in Instellingen in om deze uitnodiging te bekijken.",
|
||||
"descriptionBlockUnknownConnections": "Wanneer ingeschakeld, zal deze optie automatisch verbindingen sluiten van Cwtch gebruikers die niet aan je contactlijst zijn toegevoegd.",
|
||||
"chatHistoryDefault": "Dit gesprek wordt verwijderd wanneer Cwtch wordt gesloten! Berichtgeschiedenis kan per gesprek worden ingeschakeld via het menu Instellingen rechtsboven.",
|
||||
"encryptedServerDescription": "Het versleutelen van een server met een wachtwoord beschermt deze tegen andere mensen die dit apparaat kunnen gebruiken. Versleutelde servers kunnen niet worden ontsleuteld, weergegeven of geopend totdat het juiste wachtwoord is ingevoerd om ze te ontgrendelen.",
|
||||
"settingImagePreviewsDescription": "Afbeeldingen en profielfoto's worden automatisch gedownload en voorvertongingen gemaakt. We raden u aan dit experiment niet in te schakelen als je Cwtch gebruikt met niet-vertrouwde contacten.",
|
||||
"torSettingsEnabledCacheDescription": "Cache de huidige gedownloade Tor-consensus om deze de volgende keer dat Cwtch wordt geopend opnieuw te gebruiken. Hierdoor kan Tor sneller starten. Indien uitgeschakeld, zal Cwtch de gegevens in de cache opschonen bij het opstarten.",
|
||||
"fileSharingSettingsDownloadFolderDescription": "Wanneer bestanden automatisch worden gedownload (bijv. afbeedingen, wanneer voorvertoningen van afbeeldingen zijn ingeschakeld), is een standaardlocatie nodig om de bestanden naar te downloaden.",
|
||||
"torSettingsUseCustomTorServiceConfigurastionDescription": "Overschrijf de standaard tor-configuratie. Waarschuwing: dit kan gevaarlijk zijn. Zet dit alleen aan als je weet wat je doet.",
|
||||
"torSettingsUseCustomTorServiceConfiguration": "Gebruik een zelf ingestelde Tor-serviceconfiguratie (torrc)",
|
||||
"descriptionACNCircuitInfo": "Uitgebreide informatie over het pad dat het anonieme communicatienetwerk gebruikt om verbinding te maken met dit gesprek.",
|
||||
"conversationNotificationPolicySettingDescription": "Meldingsgedrag voor dit gesprek beheren",
|
||||
"notificationContentSimpleEvent": "Generieke melding",
|
||||
"exportProfileTooltip": "Maak een back-up van dit profiel naar een versleuteld bestand. Het versleutelde bestand kan in een andere Cwtch-app worden geïmporteerd.",
|
||||
"blockUnknownConnectionsEnabledDescription": "Verbindingen van onbekende contacten worden geblokkeerd. Je kunt dit wijzigen in Instellingen",
|
||||
"messageEnableFileSharing": "Schakel het experiment voor het delen van bestanden in om dit bericht te bekijken.",
|
||||
"serverAutostartDescription": "Bepaalt of de applicatie de server automatisch start bij het opstarten",
|
||||
"serverDescriptionDescription": "Je beschrijving van de server alleen voor persoonlijk beheer, zal nooit worden gedeeld",
|
||||
"membershipDescription": "Hieronder staat een lijst met gebruikers die berichten naar de groep hebben gestuurd. Deze lijst bevat mogelijk niet alle gebruikers die toegang hebben tot de groep.",
|
||||
"defaultScalingText": "Standaardtekstgrootte (schaalfactor:",
|
||||
"fileSharingSettingsDownloadFolderTooltip": "Bladeren om een andere standaardmap voor gedownloade bestanden te selecteren.",
|
||||
"fileDownloadUnavailable": "Dit bestand lijkt niet beschikbaar om te downloaden. Het kan zijn dat de verzender downlaoden voor dit bestand heeft uitgeschakeld.",
|
||||
"errorDownloadDirectoryDoesNotExist": "Bestanden delen kan niet worden ingeschakeld omdat de downloadmap niet is ingesteld of is ingesteld op een map die niet bestaat.",
|
||||
"settingTheme": "Lichte thema's gebruiken",
|
||||
"noPasswordWarning": "Het niet gebruiken van een wachtwoord voor dit account betekent dat alle lokaal opgeslagen gegevens niet worden versleuteld",
|
||||
"savePeerHistoryDescription": "Bepaalt of geschiedenis gekoppeld aan de contactpersoon moet worden verwijderd.",
|
||||
"dmTooltip": "Klik voor DM",
|
||||
"debugLog": "Zet console debug logging aan",
|
||||
"addContactFirst": "Voeg een contact toe of kies een contact om te beginnen met chatten.",
|
||||
"experimentClickableLinksDescription": "Het klikbare links experiment maakt het mogelijk op URLs te klikken in berichten",
|
||||
"enableExperimentClickableLinks": "Klikbare links inschakelen",
|
||||
"groupsOnThisServerLabel": "Groepen waarin ik zit gehost op deze server",
|
||||
"displayNameTooltip": "Voer een weergavenaam in",
|
||||
"tooltipBackToMessageEditing": "Terug naar bericht bewerken",
|
||||
"editServerTitle": "Server bewerken",
|
||||
"editProfile": "Bewerk profiel",
|
||||
"manageKnownServersButton": "Beheer bekende servers",
|
||||
"manageKnownServersLong": "Beheer bekende servers",
|
||||
"themeNameVampire": "Vampier",
|
||||
"sendAnInvitation": "Je hebt een uitnodiging gestuurd voor: ",
|
||||
"contactSuggestion": "Dit is een contactsuggestie voor: ",
|
||||
"successfullAddedContact": "Succesvol toegevoegd ",
|
||||
"saveProfileBtn": "Profiel opslaan",
|
||||
"deleteProfileBtn": "Verwijder profiel",
|
||||
"clickableLinksWarning": "Het openen van deze URL zal een toepassing buiten Cwtch opstarten en kan metadata onthullen of anderszins de beveiliging van Cwtch in gevaar brengen. Open alleen URL's van mensen die je vertrouwt. Weet je zeker dat je door wilt gaan?",
|
||||
"clickableLinkError": "Fout opgetreden bij het openen van URL",
|
||||
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Voor deze functie moet het Groepen experiment zijn ingeschakeld in Instellingen",
|
||||
"formattingExperiment": "Berichtopmaak",
|
||||
"successfullyImportedProfile": "Profiel succesvol geïmporteerd: %profile",
|
||||
"failedToImportProfile": "Fout bij importeren profiel",
|
||||
"notificationContentContactInfo": "Gespreksinformatie",
|
||||
"notificationPolicySettingLabel": "Beleid voor meldingen",
|
||||
"conversationNotificationPolicySettingLabel": "Beleid voor gespreksmeldingen",
|
||||
"notificationContentSettingDescription": "Regelt de inhoud van gespreksmeldingen",
|
||||
"notificationPolicySettingDescription": "Regelt het standaard meldingsgedrag van de applicatie",
|
||||
"notificationContentSettingLabel": "Inhoud van meldingen",
|
||||
"notificationPolicyOptIn": "Opt In",
|
||||
"torSettingsErrorSettingPort": "Poortnummer moet liggen tussen 1 en 65535",
|
||||
"storageMigrationModalMessage": "Profielen migreren naar een nieuw opslagformaat. Dit kan een paar minuten duren...",
|
||||
"settingImagePreviews": "Voorvertoningen van afbeeldingen en profielfoto's",
|
||||
"settingUIColumnPortrait": "UI-kolommen in portret modus",
|
||||
"settingUIColumnOptionSame": "Zelfde als portret modus instelling",
|
||||
"settingUIColumnLandscape": "UI-kolommen in liggende modus",
|
||||
"tooltipRemoveThisQuotedMessage": "Verwijder geciteerd bericht.",
|
||||
"tooltipReplyToThisMessage": "Reageer op dit bericht",
|
||||
"shutdownCwtchDialog": "Weet je zeker dat je Cwtch wilt afsluiten? Alle verbindingen en de applicatie worden gesloten.",
|
||||
"malformedMessage": "Misvormd bericht",
|
||||
"profileDeleteSuccess": "Profiel succesvol verwijderd",
|
||||
"importProfileTooltip": "Gebruik een versleutelde Cwtch backup om een profiel binnen te halen dat in een andere instantie van Cwtch is aangemaakt.",
|
||||
"unlockProfileTip": "Maak of ontgrendel een profiel om te beginnen!",
|
||||
"unlockServerTip": "Maak of ontgrendel een server om te beginnen!",
|
||||
"createProfileToBegin": "Maak of ontgrendel een profiel om te beginnen",
|
||||
"addServerFirst": "Je moet een server toevoegen voordat je een groep kunt aanmaken",
|
||||
"createProfileBtn": "Profiel aanmaken",
|
||||
"tooltipUnlockProfiles": "Ontgrendel versleutelde profielen door hun wachtwoord in te voeren.",
|
||||
"addNewItem": "Voeg een nieuw item toe aan de lijst",
|
||||
"addListItem": "Een nieuw lijstitem toevoegen",
|
||||
"peerOfflineMessage": "Contact is offline, berichten kunnen nu niet worden afgeleverd",
|
||||
"acceptGroupInviteLabel": "Wil je de uitnodiging accepteren voor",
|
||||
"profileOnionLabel": "Stuur dit adres naar contacten waarmee je in contact wilt komen",
|
||||
"password2Label": "Wachtwoord opnieuw invoeren",
|
||||
"nickChangeSuccess": "Profielnaam succesvol gewijzigd",
|
||||
"inviteToGroup": "Je bent uitgenodigd om lid te worden van een groep:",
|
||||
"messageFormattingDescription": "Rijke tekstopmaak in getoonde berichten inschakelen, bijv. **vetgedrukt** en *cursief*",
|
||||
"labelACNCircuitInfo": "ACN circuitinfo",
|
||||
"torSettingsCustomControlPortDescription": "Gebruik zelf ingestelde poort voor controleverbindingen naar de Tor proxy",
|
||||
"torSettingsCustomControlPort": "Zelf ingestelde control port",
|
||||
"torSettingsCustomSocksPortDescription": "Gebruik een zelf ingestelde poort voor dataverbindingen naar de Tor proxy",
|
||||
"torSettingsCustomSocksPort": "Zelf ingesltelde SOCKS-poort",
|
||||
"torSettingsEnabledAdvancedDescription": "Gebruik een bestaande Tor-service op je systeem of wijzig de parameters van de Cwtch Tor-service",
|
||||
"torSettingsEnabledAdvanced": "Geavanceerde Tor-configuratie inschakelen",
|
||||
"msgAddToAccept": "Voeg dit account toe aan je contacten om dit bestand te accepteren.",
|
||||
"msgConfirmSend": "Weet je zeker dat je wilt verzenden",
|
||||
"msgFileTooBig": "Bestanden mogen niet groter zijn dan 10 GB",
|
||||
"settingDownloadFolder": "Map downloaden",
|
||||
"importLocalServerSelectText": "Selecteer lokale server",
|
||||
"importLocalServerLabel": "Lokaal gehoste server importeren",
|
||||
"verfiyResumeButton": "Verifiëren\/hervatten",
|
||||
"fileCheckingStatus": "Downloadstatus controleren",
|
||||
"fileInterrupted": "Onderbroken",
|
||||
"fileSavedTo": "Opgeslagen in",
|
||||
"deleteServerConfirmBtn": "Server echt verwijderen",
|
||||
"deleteServerSuccess": "Server succesvol verwijderd",
|
||||
"enterCurrentPasswordForDeleteServer": "Voer huidige wachtwoord in om deze server te verwijderen",
|
||||
"settingServersDescription": "Het servers hosten experiment maakt het mogelijk Cwtch-servers te hosten en te beheren",
|
||||
"settingServers": "Servers hosten",
|
||||
"enterServerPassword": "Voer wachtwoord in om server te ontgrendelen",
|
||||
"serversManagerTitleLong": "Servers die je host",
|
||||
"serverAutostartLabel": "Automatisch starten",
|
||||
"serverEnabled": "Server ingeschakeld",
|
||||
"messageFileOffered": "Contact biedt aan om je een bestand te sturen",
|
||||
"contactGoto": "Ga naar gesprek met %1",
|
||||
"tooltipOpenSettings": "Open het instellingenpaneel",
|
||||
"titleManageProfiles": "Cwtch profielen beheren",
|
||||
"passwordChangeError": "Fout bij wijzigen wachtwoord: opgegeven wachtwoord geweigerd",
|
||||
"passwordErrorEmpty": "Wachtwoord mag niet leeg zijn",
|
||||
"peerNotOnline": "Contact is offline. De toepassingen kunnen op dit moment niet worden gebruikt.",
|
||||
"manageSharedFiles": "Gedeelde bestanden beheren",
|
||||
"tooltipPreviewFormatting": "Voorbeeld van berichtopmaak bekijken",
|
||||
"tooltipItalicize": "Cursief",
|
||||
"okButton": "Oké",
|
||||
"tooltipBoldText": "Vetgedrukt",
|
||||
"shareProfileMenuTooltop": "Profiel delen via...",
|
||||
"tooltipUnpinConversation": "Gesprek bovenaan 'Gesprekken' losmaken",
|
||||
"tooltipPinConversation": "Gesprek bovenaan 'Gesprekken' vastprikken",
|
||||
"replyingTo": "Reageren op %1",
|
||||
"viewReplies": "Reacties op dit bericht bekijken",
|
||||
"messageNoReplies": "Er zijn geen reacties op dit bericht.",
|
||||
"stopSharingFile": "Stop delen bestand",
|
||||
"restartFileShare": "Start delen bestand",
|
||||
"shuttingDownApp": "Afsluiten...",
|
||||
"importProfile": "Profiel importeren",
|
||||
"exportProfile": "Profiel exporteren",
|
||||
"localeDa": "Deens \/ Dansk",
|
||||
"localeCy": "Welsh \/ Cymraeg",
|
||||
"rejected": "Geweigerd!",
|
||||
"tooltipRejectContactRequest": "Weiger dit contactverzoek",
|
||||
"tooltipAcceptContactRequest": "Accepteer dit contactverzoek.",
|
||||
"notificationNewMessageFromGroup": "Nieuw bericht in een groep!",
|
||||
"notificationNewMessageFromPeer": "Nieuw bericht van een contact!",
|
||||
"accepted": "Geaccepteerd!",
|
||||
"reallyLeaveThisGroupPrompt": "Weet je zeker dat je dit gesprek wilt verlaten? Alle berichten en attributen worden verwijderd.",
|
||||
"yesLeave": "Ja, verlaat dit gesprek",
|
||||
"leaveConversation": "Verlaat dit gesprek",
|
||||
"titleManageServers": "Servers beheren",
|
||||
"invalidImportString": "Ongeldige import string",
|
||||
"enterCurrentPasswordForDelete": "Voer huidige wachtwoord in om dit profiel te verwijderen.",
|
||||
"enableGroups": "Groepschat inschakelen",
|
||||
"networkStatusConnecting": "Verbinding maken met netwerk en contacten...",
|
||||
"networkStatusAttemptingTor": "Proberen verbinding te maken met het Tor-netwerk",
|
||||
"networkStatusDisconnected": "Verbinding met internet verbroken, controleer je verbinding",
|
||||
"viewGroupMembershipTooltip": "Groepslidmaatschap weergeven",
|
||||
"zoomLabel": "Interface zoom (beïnvloedt vooral de grootte van tekst en knoppen)",
|
||||
"error0ProfilesLoadedForPassword": "0 profielen geladen met dat wachtwoord",
|
||||
"enterProfilePassword": "Voer een wachtwoord in om je profielen te bekijken",
|
||||
"addNewProfileBtn": "Nieuw profiel toevoegen",
|
||||
"deleteProfileConfirmBtn": "Profiel echt verwijderen",
|
||||
"deleteConfirmLabel": "Typ VERWIJDEREN om te bevestigen",
|
||||
"couldNotSendMsgError": "Kan bericht niet verzenden",
|
||||
"retrievingManifestMessage": "Bestandsinformatie ophalen...",
|
||||
"streamerModeLabel": "Streamer-\/presentatiemodus",
|
||||
"archiveConversation": "Archiveer dit gesprek",
|
||||
"blockedMessageMessage": "Dit bericht is van een profiel dat je hebt geblokkeerd.",
|
||||
"placeholderEnterMessage": "Type een bericht...",
|
||||
"deleteProfileSuccess": "Profiel succesvol verwijderd",
|
||||
"sendInvite": "Een contact- of groepsuitnodiging verzenden",
|
||||
"sendMessage": "Bericht versturen",
|
||||
"resetTor": "Resetten",
|
||||
"tooltipAddContact": "Een nieuw contact of gesprek toevoegen",
|
||||
"contactAlreadyExists": "Contact bestaat al",
|
||||
"conversationSettings": "Gespreksinstellingen",
|
||||
"settingFileSharing": "Bestanden delen",
|
||||
"messageFileSent": "Je hebt een bestand verzonden",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Doorhalen",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"localeNl": "Nederlands \/ Dutch",
|
||||
"pendingLabel": "In afwachting",
|
||||
"builddate": "Gebouwd op: %2",
|
||||
"versionTor": "Versie %1 met tor %2",
|
||||
"blockUnknownLabel": "Blokkeer onbekende contacten",
|
||||
"versionBuilddate": "Versie: %1 Gebouwd op: %2",
|
||||
"defaultProfileName": "Alice",
|
||||
"update": "Update",
|
||||
"viewServerInfo": "Serverinfo",
|
||||
"serverNotSynced": "Nieuwe berichten synchroniseren (dit kan even duren)...",
|
||||
"serverConnectivityDisconnected": "Server verbinding verbroken",
|
||||
"pasteAddressToAddContact": "Plak hier een cwtch adres, uitnodiging of sleutelbundel om een nieuw gesprek toe te voegen",
|
||||
"todoPlaceholder": "Todo...",
|
||||
"experimentsEnabled": "Experimenten inschakelen",
|
||||
"settingInterfaceZoom": "Zoomniveau",
|
||||
"yourDisplayName": "Jouw weergavenaam",
|
||||
"profileName": "Weergavenaam",
|
||||
"addProfileTitle": "Nieuw profiel toevoegen",
|
||||
"editProfileTitle": "Bewerk profiel",
|
||||
"copiedToClipboardNotification": "Gekopieerd naar klembord",
|
||||
"puzzleGameBtn": "Puzzelspel",
|
||||
"bulletinsBtn": "Bulletins",
|
||||
"addListItemBtn": "Item toevoegen",
|
||||
"searchList": "Zoeklijst",
|
||||
"postNewBulletinLabel": "Nieuw bulletin plaatsen",
|
||||
"newBulletinLabel": "Nieuw bulletin",
|
||||
"joinGroupTab": "Lid worden van een groep",
|
||||
"joinGroup": "Lid worden van groep",
|
||||
"settingsGroupExperiments": "Experimenten",
|
||||
"settingsGroupAppearance": "Uiterlijk",
|
||||
"settingGroupBehaviour": "Gedrag",
|
||||
"torSettingsEnableCache": "Cache Tor-consensus",
|
||||
"serverTotalMessagesLabel": "Totaal aantal berichten",
|
||||
"serverMetricsLabel": "Serverstatistieken",
|
||||
"manageKnownServersShort": "Servers",
|
||||
"newMessagesLabel": "Nieuwe berichten",
|
||||
"localeRU": "Russisch \/ Русский",
|
||||
"copyServerKeys": "Sleutels kopiëren",
|
||||
"shutdownCwtchAction": "Cwtch afsluiten",
|
||||
"shutdownCwtchTooltip": "Cwtch afsluiten",
|
||||
"shutdownCwtchDialogTitle": "Cwtch afsluiten?",
|
||||
"serverSynced": "Gesynchroniseerd",
|
||||
"defaultGroupName": "Geweldige groep",
|
||||
"shareMenuQRCode": "Toon QR-code",
|
||||
"localeTr": "Turks \/ Türk",
|
||||
"localeEl": "Grieks \/ Ελληνικά",
|
||||
"localeNo": "Noors \/ Norsk",
|
||||
"localeLb": "Luxemburgs \/ Lëtzebuergesch",
|
||||
"localeRo": "Roemeens \/ Română",
|
||||
"newMessageNotificationConversationInfo": "Nieuw bericht van %1",
|
||||
"newMessageNotificationSimple": "Nieuw bericht",
|
||||
"notificationPolicyMute": "Dempen",
|
||||
"labelTorNetwork": "Tor netwerk",
|
||||
"btnSendFile": "Bestand versturen",
|
||||
"loadingCwtch": "Cwtch laden...",
|
||||
"themeColorLabel": "Kleurenthema",
|
||||
"themeNameNeon2": "Neon2",
|
||||
"themeNameNeon1": "Neon1",
|
||||
"themeNameMidnight": "Middernacht",
|
||||
"themeNameMermaid": "Zeemeermin",
|
||||
"themeNamePumpkin": "Pompoen",
|
||||
"themeNameGhost": "Geest",
|
||||
"themeNameWitch": "Heks",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"serverConnectionsLabel": "Verbinding",
|
||||
"copyAddress": "Adres kopiëren",
|
||||
"saveServerButton": "Server opslaan",
|
||||
"serverEnabledDescription": "Start of stop de server",
|
||||
"serverDescriptionLabel": "Server beschrijving",
|
||||
"serverAddress": "Server adres",
|
||||
"titleManageProfilesShort": "Profielen",
|
||||
"tooltipSendFile": "Verstuur bestand",
|
||||
"labelFilesize": "Grootte",
|
||||
"labelFilename": "Bestandsnaam",
|
||||
"openFolderButton": "Open map",
|
||||
"torNetworkStatus": "Tor netwerk status",
|
||||
"localeDe": "Duits \/ Deutsch",
|
||||
"localePt": "Portugees \/ Portuguesa",
|
||||
"localeFr": "Frans \/ Français",
|
||||
"localeEn": "Engels \/ English",
|
||||
"unlock": "Ontgrendelen",
|
||||
"radioNoPassword": "Onversleuteld (Geen wachtwoord)",
|
||||
"newProfile": "Nieuw profiel",
|
||||
"inviteToGroupLabel": "Uitnodigen voor groep",
|
||||
"newGroupBtn": "Nieuwe groep aanmaken",
|
||||
"createGroupBtn": "Aanmaken",
|
||||
"createGroupTitle": "Groep aanmaken",
|
||||
"unblockBtn": "Contact deblokkeren",
|
||||
"cancel": "Annuleren",
|
||||
"serversManagerTitleShort": "Servers",
|
||||
"showMessageButton": "Toon bericht",
|
||||
"settingUIColumnDouble14Ratio": "Dubbel (1:4)",
|
||||
"settingUIColumnDouble12Ratio": "Dubbel (1:2)",
|
||||
"settingUIColumnSingle": "Enkel",
|
||||
"localePl": "Pools \/ Polski",
|
||||
"tooltipHidePassword": "Verberg wachtwoord",
|
||||
"tooltipShowPassword": "Toon wachtwoord",
|
||||
"torStatus": "Tor status",
|
||||
"torVersion": "Tor versie",
|
||||
"newPassword": "Nieuw wachtwoord",
|
||||
"titleManageContacts": "Gesprekken",
|
||||
"localeIt": "Italiaans \/ taliano",
|
||||
"localeEs": "Spaans \/ Español",
|
||||
"newConnectionPaneTitle": "Nieuwe verbinding",
|
||||
"networkStatusOnline": "Online",
|
||||
"loadingTor": "Tor laden...",
|
||||
"smallTextLabel": "Klein",
|
||||
"largeTextLabel": "Groot",
|
||||
"settingLanguage": "Taal",
|
||||
"yourServers": "Jouw servers",
|
||||
"yourProfiles": "Jouw profielen",
|
||||
"password": "Wachtwoord",
|
||||
"passwordErrorMatch": "Wachtwoorden komen niet overeen",
|
||||
"clickableLinksCopy": "Kopieer URL",
|
||||
"clickableLinkOpen": "Open URL",
|
||||
"addServerTooltip": "Nieuwe server toevoegen",
|
||||
"addServerTitle": "Server toevoegen",
|
||||
"importLocalServerButton": "Importeer %1",
|
||||
"headingReplies": "Reacties",
|
||||
"fieldDescriptionLabel": "Beschrijving",
|
||||
"enableExperimentQRCode": "QR-codes",
|
||||
"downloadFileButton": "Download",
|
||||
"deleteConfirmText": "VERWIJDER",
|
||||
"deleteBtn": "Verwijderen",
|
||||
"cwtchSettingsTitle": "Cwtch instellingen",
|
||||
"conversationNotificationPolicyNever": "Nooit",
|
||||
"conversationNotificationPolicyDefault": "Standaard",
|
||||
"addContactConfirm": "Contact %1 toevoegen",
|
||||
"addContact": "Contact toevoegen",
|
||||
"version": "Versie %1",
|
||||
"themeDark": "Donker",
|
||||
"themeLight": "Licht",
|
||||
"currentPasswordLabel": "Huidig wachtwoord",
|
||||
"password1Label": "Wachtwoord",
|
||||
"radioUsePassword": "Wachtwoord",
|
||||
"savePeerHistory": "Geschiedenis opslaan",
|
||||
"dontSavePeerHistory": "Geschiedenis verwijderen",
|
||||
"displayNameLabel": "Weergavenaam",
|
||||
"rejectGroupBtn": "Weigeren",
|
||||
"acceptGroupBtn": "Accepteren",
|
||||
"copyBtn": "Kopieer",
|
||||
"peerBlockedMessage": "Contact is geblokkeerd",
|
||||
"blockBtn": "Blokkeer contact",
|
||||
"saveBtn": "Opslaan",
|
||||
"addressLabel": "Adres",
|
||||
"listsBtn": "Lijsten",
|
||||
"chatBtn": "Chat",
|
||||
"inviteBtn": "Uitnodigen",
|
||||
"groupNameLabel": "Groepsnaam",
|
||||
"serverConnectivityConnected": "Server verbonden",
|
||||
"serverInfo": "Server informatie",
|
||||
"titlePlaceholder": "titel...",
|
||||
"createGroup": "Groep aanmaken",
|
||||
"createGroupTab": "Nieuwe groep",
|
||||
"peerAddress": "Adres",
|
||||
"peerName": "Naam",
|
||||
"blocked": "Geblokkeerd",
|
||||
"search": "Zoeken...",
|
||||
"serverLabel": "Server",
|
||||
"invitationLabel": "Uitnodiging",
|
||||
"addPeer": "Contact toevoegen",
|
||||
"groupAddr": "Adres",
|
||||
"server": "Server",
|
||||
"invitation": "Uitnodiging",
|
||||
"addPeerTab": "Contact toevoegen"
|
||||
}
|
|
@ -1,17 +1,71 @@
|
|||
{
|
||||
"@@locale": "no",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Tyrkisk \/ Türk",
|
||||
"localeIt": "Italiensk",
|
||||
"localeEn": "Engelsk \/ English",
|
||||
"localeFr": "Fransk \/ Français",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Tysk \/ Deutsch",
|
||||
"localeLb": "Luxemburgsk",
|
||||
"localeNo": "Norsk",
|
||||
"localeEl": "Gresk",
|
||||
"localePl": "Polsk",
|
||||
"localeRo": "Rumensk",
|
||||
"localeRU": "Russisk",
|
||||
"localeEs": "Spansk",
|
||||
"localeDa": "Dansk",
|
||||
"localePt": "Portugisisk",
|
||||
"localeCy": "Walisisk",
|
||||
"settingImagePreviewsDescription": "Bilder vil bli lastet ned og forhåndsvist automatisk. Merk at forhåndsvisning av bilder kan medføre en sikkerhetsrisiko og bør ikke tillates dersom du bruker Cwtch med kontakter du ikke stoler på. Profilbilder er planlagt for Cwtch 1.6.",
|
||||
"tooltipPreviewFormatting": "Preview Message Formatting",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipItalicize": "Italic",
|
||||
"tooltipBackToMessageEditing": "Back to Message Editing",
|
||||
"tooltipBoldText": "Bold",
|
||||
"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",
|
||||
"serverLabel": "Tjener",
|
||||
"profileOnionLabel": "Del denne adressen med de du ønsker å kontakte",
|
||||
"copiedToClipboardNotification": "Kopiert til utklippstavle",
|
||||
"saveBtn": "Lagre",
|
||||
"deleteBtn": "Slett",
|
||||
"localeEl": "Gresk",
|
||||
"localeNo": "Norsk",
|
||||
"localeLb": "Luxemburgsk",
|
||||
"localeRo": "Rumensk",
|
||||
"newMessageNotificationConversationInfo": "Ny melding fra %1",
|
||||
"newMessageNotificationSimple": "Ny melding",
|
||||
"notificationContentContactInfo": "Samtaleinformasjon",
|
||||
|
@ -65,7 +119,6 @@
|
|||
"themeNameWitch": "Heks",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingDownloadFolder": "Nedlastningsmappe",
|
||||
"settingImagePreviewsDescription": "Bilder vil bli lastet ned og forhåndsvist automatisk. Merk at forhåndsvisning av bilder kan medføre en sikkerhetsrisiko og bør ikke tillates dersom du bruker Cwtch med kontakter du ikke stoler på. Profilbilder er planlagt for Cwtch 1.6.",
|
||||
"settingImagePreviews": "Forhåndsvisning og profilbilder",
|
||||
"experimentClickableLinksDescription": "Forsøket klikkbare lenker lar deg klikke på URLer i meldinger",
|
||||
"enableExperimentClickableLinks": "Tillat klikkbare lenker",
|
||||
|
@ -82,7 +135,6 @@
|
|||
"importLocalServerSelectText": "Velg lokal tjener",
|
||||
"importLocalServerLabel": "Importér en lokal tjener",
|
||||
"newMessagesLabel": "Nye meldinger",
|
||||
"localeRU": "Russisk",
|
||||
"copyServerKeys": "Kopiér nøkler",
|
||||
"verfiyResumeButton": "Bekreft\/fortsett",
|
||||
"fileCheckingStatus": "Tester nedlastningsstatus",
|
||||
|
@ -142,7 +194,6 @@
|
|||
"settingUIColumnSingle": "Enkel",
|
||||
"settingUIColumnLandscape": "UI kolonner i landskapsmodus",
|
||||
"settingUIColumnPortrait": "UI kolonner i portrettmodus",
|
||||
"localePl": "Polsk",
|
||||
"tooltipRemoveThisQuotedMessage": "Fjern sitert melding.",
|
||||
"tooltipReplyToThisMessage": "Svar til denne meldingen",
|
||||
"tooltipRejectContactRequest": "Avvis denne kontaktsforespørselen",
|
||||
|
@ -196,8 +247,6 @@
|
|||
"conversationSettings": "Samtaleinnstillinger",
|
||||
"enterCurrentPasswordForDelete": "Vennligst oppgi tilhørende passord for å slette denne profilen.",
|
||||
"enableGroups": "Aktiver gruppesamtaler",
|
||||
"localeIt": "Italiensk",
|
||||
"localeEs": "Spansk",
|
||||
"todoPlaceholder": "Gjøremål...",
|
||||
"addNewItem": "Legg til nyt lelement til listen",
|
||||
"addListItem": "Legg til nytt listeelement",
|
||||
|
@ -219,10 +268,6 @@
|
|||
"settingTheme": "Velg lyst tema",
|
||||
"largeTextLabel": "Stor",
|
||||
"settingInterfaceZoom": "Zoomnivå",
|
||||
"localeDe": "Tysk",
|
||||
"localePt": "Portugisisk",
|
||||
"localeFr": "Fransk",
|
||||
"localeEn": "Engelsk",
|
||||
"settingLanguage": "Språk",
|
||||
"blockUnknownLabel": "Blokkér ukjente kontakter",
|
||||
"zoomLabel": "Grensesnittsforstørrelse (påvirker tekst og knappestørrelse)",
|
||||
|
|
|
@ -1,55 +1,120 @@
|
|||
{
|
||||
"@@locale": "pl",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Turecki \/ Türk",
|
||||
"localeIt": "Włoski \/ Italiano",
|
||||
"localeEn": "Angielski \/ English",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Niemiecki \/ Deutsch",
|
||||
"serverLabel": "Server",
|
||||
"deleteBtn": "Usuń",
|
||||
"saveBtn": "Zapisz",
|
||||
"editProfile": "Edytuj profil",
|
||||
"tooltipSubscript": "Indeks",
|
||||
"tooltipBackToMessageEditing": "Powrót do edycji wiadomości",
|
||||
"tooltipPreviewFormatting": "Podgląd Formatowanie wiadomości",
|
||||
"tooltipBoldText": "Pogrubiona",
|
||||
"tooltipItalicize": "Kursywa",
|
||||
"profileOnionLabel": "Przekaż ten adres osobom, z którymi chcesz nawiązać kontakt",
|
||||
"tooltipSuperscript": "Superskrypt",
|
||||
"tooltipCode": "Kod \/ Monospace",
|
||||
"settingImagePreviewsDescription": "Obrazy i zdjęcia profilowe będą pobierane i przeglądane automatycznie. Zalecamy, aby nie włączać tego eksperymentu, jeśli używasz Cwtch z niezaufanymi kontaktami.",
|
||||
"localeNo": "Norweski \/ Norsk",
|
||||
"localePt": "Portugalski \/ Portuguesa",
|
||||
"settingTheme": "Użyj motywów świetlnych",
|
||||
"localePl": "Polski \/ Polski",
|
||||
"localeFr": "Francuski \/ Français",
|
||||
"localeDa": "Duński \/ Dansk",
|
||||
"localeCy": "Walijski \/ Cymraeg",
|
||||
"tooltipStrikethrough": "Przekreślenie",
|
||||
"localeLb": "Luksemburski \/ Lëtzebuergesch",
|
||||
"localeEl": "Grecki \/ Ελληνικά",
|
||||
"localeRo": "Rumuński \/ Română",
|
||||
"localeEs": "Hiszpański \/ Español",
|
||||
"localeRU": "Rosyjski \/ Русский",
|
||||
"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",
|
||||
"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",
|
||||
"archiveConversation": "Zarchiwizuj tę konwersację",
|
||||
"groupInviteSettingsWarning": "Zaproszono Cię do grupy! Aby wyświetlić to zaproszenie, włącz Grupy (eksperymentalne) w Ustawieniach",
|
||||
"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.",
|
||||
"invalidImportString": "Niepoprawny ciąg importu",
|
||||
"enableGroups": "Włącz Grupy",
|
||||
"todoPlaceholder": "Do zrobienia...",
|
||||
"copiedToClipboardNotification": "Skopiowano do schowka",
|
||||
"searchList": "Lista wyszukiwania",
|
||||
"deleteConfirmLabel": "Wpisz USUŃ aby potwierdzić",
|
||||
"localeLb": "Luksemburski",
|
||||
"localeNo": "Norweski",
|
||||
"localeEl": "Grecki",
|
||||
"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",
|
||||
"largeTextLabel": "Duży",
|
||||
"settingInterfaceZoom": "Przybliżenie",
|
||||
"localeDe": "Deutsche",
|
||||
"localePt": "Portuguesa",
|
||||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Język",
|
||||
"blockUnknownLabel": "Blokuj nieznajomych",
|
||||
"zoomLabel": "Przybliżenie interfejsu (wpływa głównie na rozmiar tekstu i przycisków)",
|
||||
|
@ -74,23 +139,19 @@
|
|||
"password1Label": "Hasło",
|
||||
"currentPasswordLabel": "Obecne hasło",
|
||||
"yourDisplayName": "Nazwa",
|
||||
"profileOnionLabel": "Send this address to contacts you want to connect with",
|
||||
"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",
|
||||
"editProfile": "Edytuj profil",
|
||||
"newProfile": "Nowy profil",
|
||||
"defaultProfileName": "Nowy profil",
|
||||
"profileName": "Nazwa",
|
||||
"editProfileTitle": "Edytuj profil",
|
||||
"addProfileTitle": "Dodaj nowy profil",
|
||||
"deleteBtn": "Delete",
|
||||
"unblockBtn": "Odblokuj",
|
||||
"dontSavePeerHistory": "Nie",
|
||||
"savePeerHistoryDescription": "Zapisywanie wiadomości",
|
||||
"savePeerHistory": "Tak",
|
||||
"blockBtn": "Zablokuj",
|
||||
"saveBtn": "Save",
|
||||
"displayNameLabel": "Nazwa",
|
||||
"addressLabel": "Adresy",
|
||||
"puzzleGameBtn": "Puzzle",
|
||||
|
@ -111,7 +172,6 @@
|
|||
"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",
|
||||
"update": "Zaktualizuj",
|
||||
"inviteBtn": "Zaproś",
|
||||
"inviteToGroupLabel": "Zaproś do grupy",
|
||||
|
@ -123,7 +183,6 @@
|
|||
"serverConnectivityConnected": "Połączono z serwerem",
|
||||
"serverInfo": "Informacje o serwerze",
|
||||
"invitationLabel": "Zaproszenie",
|
||||
"serverLabel": "Server",
|
||||
"search": "Szukaj...",
|
||||
"blocked": "Zablokowany",
|
||||
"pasteAddressToAddContact": "Wklej adres Cwtch znajomego, zaproszenie do grupy albo pęk kluczy",
|
||||
|
@ -161,7 +220,6 @@
|
|||
"themeNameWitch": "Czarownica",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingDownloadFolder": "Folder dla pobranych plików",
|
||||
"settingImagePreviewsDescription": "Automatyczne pobieranie i podgląd obrazów. Pamiętaj, że podgląd obrazów jest potencjalną luką w zabezpieczeniach i nie należy używać tej eksperymentalnej funkcjonalności jeśli używasz Cwtch do komunikacji z niezaufanymi osobami. Zdjęcia profilowe są przewidziane na wersję Cwtch 1.6",
|
||||
"settingImagePreviews": "Podgląd zdjęć i zdjęcia profilowe",
|
||||
"experimentClickableLinksDescription": "Klikalne linki (eksperymentalne). Umożliwia klikanie na linki w wiadomościach, aby je otworzyć",
|
||||
"enableExperimentClickableLinks": "Włącz klikalne linki",
|
||||
|
@ -207,7 +265,6 @@
|
|||
"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",
|
||||
"shutdownCwtchDialog": "Zamknąć Cwtch? Wszystkie połączenia zostaną zakończone, a aplikacja zostanie zamknięta.",
|
||||
"malformedMessage": "Wiadomość uszkodzona",
|
||||
"profileDeleteSuccess": "Profil został usunięty",
|
||||
|
@ -230,21 +287,14 @@
|
|||
"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.",
|
||||
"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",
|
||||
"conversationSettings": "Ustawienia konwersacji",
|
||||
"enterCurrentPasswordForDelete": "Aby usunąć ten profil, wprowadź hasło.",
|
||||
"enableGroups": "Włącz czaty grupowe",
|
||||
"localeIt": "Italiana",
|
||||
"localeEs": "Espanol",
|
||||
"todoPlaceholder": "Do zdobienia...",
|
||||
"addNewItem": "Dodaj do listy",
|
||||
"addListItem": "Add a New List Item",
|
||||
"newConnectionPaneTitle": "Nowe połączenie",
|
||||
|
@ -262,7 +312,6 @@
|
|||
"experimentsEnabled": "Włącz funkcje eksperymentalne",
|
||||
"themeDark": "Ciemny",
|
||||
"themeLight": "Jasny",
|
||||
"settingTheme": "Motyw",
|
||||
"titleManageServers": "Zarządzaj serwerami",
|
||||
"newPassword": "Nowe hasło",
|
||||
"torVersion": "Wersja Tor",
|
||||
|
@ -297,13 +346,10 @@
|
|||
"enterServerPassword": "Wprowadź hasło, aby odblokować serwer",
|
||||
"enterCurrentPasswordForDeleteServer": "Wprowadź aktualne hasło, aby usunąć ten serwer",
|
||||
"newMessagesLabel": "Nowe wiadomości",
|
||||
"localePl": "Polski",
|
||||
"localeRU": "Rosyjski",
|
||||
"copyAddress": "Skopiuj adres",
|
||||
"fileSavedTo": "Zapisano do",
|
||||
"verfiyResumeButton": "Zweryfikuj\/wznów",
|
||||
"copyServerKeys": "Kopiuj klucze",
|
||||
"archiveConversation": "Zarchiwizuj tę rozmowę",
|
||||
"streamerModeLabel": "Tryb streamera\/prezentacji",
|
||||
"retrievingManifestMessage": "Pobieranie informacji o pliku...",
|
||||
"openFolderButton": "Otwórz folder",
|
||||
|
@ -312,6 +358,5 @@
|
|||
"labelFilesize": "Rozmiar",
|
||||
"messageFileSent": "Plik został wysłany",
|
||||
"tooltipSendFile": "Wyślij plik",
|
||||
"settingFileSharing": "Udostępnianie plików",
|
||||
"copiedToClipboardNotification": "Copied to Clipboard"
|
||||
"settingFileSharing": "Udostępnianie plików"
|
||||
}
|
|
@ -1,12 +1,66 @@
|
|||
{
|
||||
"@@locale": "pt",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"localeDa": "Danish",
|
||||
"localeCy": "Welsh",
|
||||
"localeEl": "Greek",
|
||||
"localeNo": "Norwegian",
|
||||
"localeLb": "Luxembourgish",
|
||||
"localeRo": "Romanian",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Turco \/ Türk",
|
||||
"localeIt": "Italian \/ Italiano",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Alemao \/ Deutsch",
|
||||
"localeEn": "English \/ English",
|
||||
"localeLb": "Luxembourgish \/ Lëtzebuergesch",
|
||||
"localeNo": "Norwegian \/ Norsk",
|
||||
"localeEl": "Greek \/ Ελληνικά",
|
||||
"localePl": "Polish \/ Polski",
|
||||
"localeRo": "Romanian \/ Română",
|
||||
"localeRU": "Russian \/ Русский",
|
||||
"localeEs": "Spanish \/ Español",
|
||||
"localeDa": "Danish \/ Dansk",
|
||||
"localePt": "Portuguese \/ Portuguesa",
|
||||
"localeFr": "French \/ Français",
|
||||
"localeCy": "Welsh \/ Cymraeg",
|
||||
"settingImagePreviewsDescription": "Images and Profile Pictures will be downloaded and previewed automatically. We recommend that you do not enable this Experiment if you use Cwtch with untrusted contacts.",
|
||||
"tooltipPreviewFormatting": "Preview Message Formatting",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipItalicize": "Italic",
|
||||
"tooltipBackToMessageEditing": "Back to Message Editing",
|
||||
"tooltipBoldText": "Bold",
|
||||
"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",
|
||||
"newMessageNotificationConversationInfo": "New Message From %1",
|
||||
"newMessageNotificationSimple": "New Message",
|
||||
"notificationContentContactInfo": "Conversation Information",
|
||||
|
@ -60,7 +114,6 @@
|
|||
"themeNameWitch": "Witch",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingDownloadFolder": "Download Folder",
|
||||
"settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.",
|
||||
"settingImagePreviews": "Image Previews and Profile Pictures",
|
||||
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
|
||||
"enableExperimentClickableLinks": "Enable Clickable Links",
|
||||
|
@ -77,7 +130,6 @@
|
|||
"importLocalServerSelectText": "Select Local Server",
|
||||
"importLocalServerLabel": "Import a locally hosted server",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
|
@ -137,7 +189,6 @@
|
|||
"settingUIColumnSingle": "Single",
|
||||
"settingUIColumnLandscape": "UI Columns in Landscape Mode",
|
||||
"settingUIColumnPortrait": "UI Columns in Portrait Mode",
|
||||
"localePl": "Polish",
|
||||
"tooltipRemoveThisQuotedMessage": "Remove quoted message.",
|
||||
"tooltipReplyToThisMessage": "Reply to this message",
|
||||
"tooltipRejectContactRequest": "Reject this contact request",
|
||||
|
@ -191,8 +242,6 @@
|
|||
"conversationSettings": "Conversation Settings",
|
||||
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
|
||||
"enableGroups": "Enable Group Chat",
|
||||
"localeIt": "Italiana",
|
||||
"localeEs": "Espanol",
|
||||
"todoPlaceholder": "Afazer…",
|
||||
"addNewItem": "Adicionar novo item à lista",
|
||||
"addListItem": "Adicionar Item à Lista",
|
||||
|
@ -214,10 +263,6 @@
|
|||
"settingTheme": "Use Light Themes",
|
||||
"largeTextLabel": "Grande",
|
||||
"settingInterfaceZoom": "Zoom level",
|
||||
"localeDe": "Deutsche",
|
||||
"localePt": "Portuguesa",
|
||||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Language",
|
||||
"blockUnknownLabel": "Block Unknown Contacts",
|
||||
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)",
|
||||
|
|
|
@ -0,0 +1,363 @@
|
|||
{
|
||||
"@@locale": "ptbr",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "O suporte a QR Code permite o compartilhamento de dados (como identidade de perfil) através dos QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Mostrar QR Code",
|
||||
"shareProfileMenuTooltop": "Compartilhar perfil por...",
|
||||
"acquiredTicketsFromServer": "Desafio anti-spam completo",
|
||||
"acquiringTicketsFromServer": "Realizar o desafio anti-spam",
|
||||
"errorDownloadDirectoryDoesNotExist": "O compartilhamento de arquivos não pode ser ativado porque a Pasta de Download não foi definida, ou está definida para uma pasta que não existe.",
|
||||
"localeTr": "Turco \/ Türk",
|
||||
"localeIt": "Italian \/ Italiano",
|
||||
"tooltipUnpinConversation": "Desafixar conversa a partir do topo de \"Conversations\"",
|
||||
"tooltipPinConversation": "Fixar conversa para o topo de \"Conversations\"",
|
||||
"replyingTo": "Responder para %1",
|
||||
"fileDownloadUnavailable": "Este arquivo aparece indisponível para download. O remetente pode ter desativado os downloads para este arquivo.",
|
||||
"messageNoReplies": "Não há respostas para esta mensagem.",
|
||||
"headingReplies": "Respostas",
|
||||
"viewReplies": "Ver respostas para esta mensagem",
|
||||
"restartFileShare": "Iniciar compartilhamento de arquivo",
|
||||
"stopSharingFile": "Parar compartilhamento de arquivo",
|
||||
"manageSharedFiles": "Gerenciar arquivos compartilhados",
|
||||
"localeDe": "Alemao \/ Deutsch",
|
||||
"localeEn": "English \/ English",
|
||||
"localeLb": "Luxembourgish \/ Lëtzebuergesch",
|
||||
"localeNo": "Norwegian \/ Norsk",
|
||||
"localeEl": "Greek \/ Ελληνικά",
|
||||
"localePl": "Polish \/ Polski",
|
||||
"localeRo": "Romanian \/ Română",
|
||||
"localeRU": "Russian \/ Русский",
|
||||
"localeEs": "Spanish \/ Español",
|
||||
"localeDa": "Danish \/ Dansk",
|
||||
"localePt": "Portuguese \/ Portuguesa",
|
||||
"localePtBr": "Brazilian Portuguese \/ Português do Brasil",
|
||||
"localeFr": "French \/ Français",
|
||||
"localeCy": "Welsh \/ Cymraeg",
|
||||
"settingImagePreviewsDescription": "Imagens e Fotos de Perfil serão baixadas e visualizadas automaticamente. Recomendamos que você não ative esta Experiência se usar Cwtch com contatos não confiáveis.",
|
||||
"tooltipPreviewFormatting": "Pré-visualização da formatação da mensagem",
|
||||
"tooltipCode": "Code \/ Monoespaçado",
|
||||
"tooltipStrikethrough": "Riscado",
|
||||
"tooltipSubscript": "Sobrescrito",
|
||||
"tooltipSuperscript": "Superescrito",
|
||||
"tooltipItalicize": "Itálico",
|
||||
"tooltipBackToMessageEditing": "Voltar a edição de mensagem",
|
||||
"tooltipBoldText": "Negrito",
|
||||
"okButton": "OK",
|
||||
"settingsAndroidPowerReenablePopup": "Não é possível reativar a Otimização da Bateria de dentro da Cwtch. Favor ir para Android \/ Settings \/ Apps \/ Cwtch \/ Battery e definir o uso como 'Otimizado'",
|
||||
"settingAndroidPowerExemptionDescription": "Opcional: Solicite ao Android que isente o Cwtch do gerenciamento otimizado de energia. Isto resultará em melhor estabilidade ao custo de maior uso da bateria.",
|
||||
"settingAndroidPowerExemption": "Ignorar Otimização de Bateria do Android",
|
||||
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Esta característica requer que a experiência de grupo seja habilitada em Configurações",
|
||||
"messageFormattingDescription": "Permitir a formatação de texto rico em mensagens exibidas, por exemplo **negrito** e *itálico*",
|
||||
"formattingExperiment": "Formatação de mensagem",
|
||||
"clickableLinkError": "Erro encontrado durante a tentativa de abrir a URL",
|
||||
"clickableLinksCopy": "Copiar URL",
|
||||
"clickableLinkOpen": "Abrir URL",
|
||||
"clickableLinksWarning": "A abertura deste URL abrirá um aplicativo fora do Cwtch e poderá revelar metadados ou comprometer de outra forma a segurança do Cwtch. Abra somente URLs de pessoas em quem você confia. Você tem certeza de que quer continuar?",
|
||||
"shuttingDownApp": "Desligando...",
|
||||
"successfullyImportedProfile": "Perfil importado com sucesso: %profile",
|
||||
"failedToImportProfile": "Erro ao importar perfil",
|
||||
"importProfileTooltip": "Use um backup criptografado do Cwtch para trazer um perfil criado em outra instância do Cwtch.",
|
||||
"importProfile": "Importar perfil",
|
||||
"exportProfileTooltip": "Faça um backup deste perfil em um arquivo criptografado. O arquivo criptografado pode ser importado para outro aplicativo Cwtch.",
|
||||
"exportProfile": "Exportar Perfil",
|
||||
"newMessageNotificationConversationInfo": "Nova mensagem de %1",
|
||||
"newMessageNotificationSimple": "Nova Mensagem",
|
||||
"notificationContentContactInfo": "Informações da Conversa",
|
||||
"notificationContentSimpleEvent": "Evento Simples",
|
||||
"conversationNotificationPolicySettingDescription": "controle de comportamento de notificação para esta conversa",
|
||||
"conversationNotificationPolicySettingLabel": "Política de Notificação de Conversas",
|
||||
"settingsGroupExperiments": "Experimentos",
|
||||
"settingsGroupAppearance": "Aparência",
|
||||
"settingGroupBehaviour": "Comportamento",
|
||||
"notificationContentSettingDescription": "Controla o conteúdo das notificações de conversas",
|
||||
"notificationPolicySettingDescription": "Controla o comportamento padrão de notificação do aplicativo",
|
||||
"notificationContentSettingLabel": "Conteúdo da Notificação",
|
||||
"notificationPolicySettingLabel": "Política de Notificação",
|
||||
"conversationNotificationPolicyNever": "Nunca",
|
||||
"conversationNotificationPolicyOptIn": "Optar por",
|
||||
"conversationNotificationPolicyDefault": "Padrão",
|
||||
"notificationPolicyDefaultAll": "Todos por padrão",
|
||||
"notificationPolicyOptIn": "Optar por",
|
||||
"notificationPolicyMute": "Silenciar",
|
||||
"tooltipSelectACustomProfileImage": "Selecionar uma imagem de perfil personalizada",
|
||||
"torSettingsEnabledCacheDescription": "Mantém em cache os dados atuais do Tor para reutilizar da próxima vez que o Cwtch for aberto. Isto permitirá que o Tor comece mais rápido. Quando desativado, o Cwtch irá eliminar os dados em cache ao iniciar.",
|
||||
"torSettingsEnableCache": "Consenso de Cache do Tor",
|
||||
"labelTorNetwork": "Rede Tor",
|
||||
"descriptionACNCircuitInfo": "Informações detalhadas sobre o caminho que a rede de comunicação anônima está utilizando para se conectar a esta conversa.",
|
||||
"labelACNCircuitInfo": "Informações do Circuito ACN",
|
||||
"fileSharingSettingsDownloadFolderTooltip": "Navegue para selecionar uma pasta padrão diferente para arquivos baixados.",
|
||||
"fileSharingSettingsDownloadFolderDescription": "Quando os arquivos são baixados automaticamente (por exemplo, prévias de imagem, quando as pré-visualizações de imagem estão habilitadas) é necessário um local padrão para o download destes arquivos.",
|
||||
"torSettingsErrorSettingPort": "O número da porta deve ser entre 1 e 65535",
|
||||
"torSettingsUseCustomTorServiceConfigurastionDescription": "Substituir a configuração padrão do Tor. Atenção: Isto pode ser perigoso. Só use isto se você souber o que está fazendo.",
|
||||
"torSettingsUseCustomTorServiceConfiguration": "Use uma configuração personalizada de serviço do Tor (torrc)",
|
||||
"torSettingsCustomControlPortDescription": "Use uma porta personalizada para conexões de controle para o proxy Tor",
|
||||
"torSettingsCustomControlPort": "Porta de controle personalizada",
|
||||
"torSettingsCustomSocksPortDescription": "Use uma porta personalizada para conexões de dados com o proxy Tor",
|
||||
"torSettingsCustomSocksPort": "Porta SOCKS personalizada",
|
||||
"torSettingsEnabledAdvancedDescription": "Use um serviço Tor existente em seu sistema, ou altere os parâmetros do serviço Cwtch Tor",
|
||||
"torSettingsEnabledAdvanced": "Habilitar configuração avançada do Tor",
|
||||
"msgAddToAccept": "Adicione esta conta a seus contatos para poder aceitar este arquivo.",
|
||||
"btnSendFile": "Enviar arquivo",
|
||||
"msgConfirmSend": "Você tem certeza de que deseja enviar",
|
||||
"msgFileTooBig": "O tamanho do arquivo não pode exceder 10 GB",
|
||||
"storageMigrationModalMessage": "Migração dos perfis para um novo formato de armazenamento. Isto pode levar alguns minutos...",
|
||||
"loadingCwtch": "Carregando o Cwtch...",
|
||||
"themeColorLabel": "Cor do tema",
|
||||
"themeNameNeon2": "Neon2",
|
||||
"themeNameNeon1": "Neon1",
|
||||
"themeNameMidnight": "Meia-noite",
|
||||
"themeNameMermaid": "Sereia",
|
||||
"themeNamePumpkin": "Abóbora",
|
||||
"themeNameGhost": "Fantasma",
|
||||
"themeNameVampire": "Vampiro",
|
||||
"themeNameWitch": "Bruxa",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingDownloadFolder": "Pasta para Download",
|
||||
"settingImagePreviews": "Pré-visualização de Imagens e Fotos de Perfil",
|
||||
"experimentClickableLinksDescription": "O experimento de links clicáveis permite que você clique em URLs compartilhadas em mensagens",
|
||||
"enableExperimentClickableLinks": "Habilitar links clicáveis",
|
||||
"serverConnectionsLabel": "Conexão",
|
||||
"serverTotalMessagesLabel": "Total de mensagens",
|
||||
"serverMetricsLabel": "Métricas do servidor",
|
||||
"manageKnownServersShort": "Servidores",
|
||||
"manageKnownServersLong": "Gerenciar Servidores Conhecidos",
|
||||
"displayNameTooltip": "Por favor, digite um nome de exibição",
|
||||
"manageKnownServersButton": "Gerenciar Servidores Conhecidos",
|
||||
"fieldDescriptionLabel": "Descrição",
|
||||
"groupsOnThisServerLabel": "Grupos nos quais estou hospedado neste servidor",
|
||||
"importLocalServerButton": "Importar %1",
|
||||
"importLocalServerSelectText": "Selecione Servidor Local",
|
||||
"importLocalServerLabel": "Importar um servidor hospedado localmente",
|
||||
"newMessagesLabel": "Novas mensagens",
|
||||
"copyServerKeys": "Copiar chaves",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Verificando o status do download",
|
||||
"fileInterrupted": "Interrompido",
|
||||
"fileSavedTo": "Salvar em",
|
||||
"encryptedServerDescription": "Criptografar um servidor com uma senha o protege de outras pessoas que também podem usar este dispositivo. Os servidores criptografados não podem ser descriptografados, exibidos ou acessados até que a senha correta seja inserida para desbloqueá-los.",
|
||||
"plainServerDescription": "Recomendamos que você proteja seus servidores Cwtch com uma senha. Se você não definir uma senha neste servidor, qualquer pessoa que tenha acesso a este dispositivo poderá acessar informações sobre este servidor, incluindo chaves criptográficas sensíveis.",
|
||||
"deleteServerConfirmBtn": "Realmente excluir servidor",
|
||||
"deleteServerSuccess": "Servidor excluído com sucesso",
|
||||
"enterCurrentPasswordForDeleteServer": "Por favor, digite a senha atual para excluir este servidor",
|
||||
"copyAddress": "Copiar endereço",
|
||||
"settingServersDescription": "A experiência com servidores de hospedagem permite hospedar e gerenciar servidores Cwtch",
|
||||
"settingServers": "Servidores Hospedados",
|
||||
"enterServerPassword": "Digite a senha para desbloquear o servidor",
|
||||
"unlockProfileTip": "Por favor, crie ou desbloqueie um perfil para começar!",
|
||||
"unlockServerTip": "Por favor, crie ou desbloqueie um servidor para começar!",
|
||||
"addServerTooltip": "Adicionar novo servidor",
|
||||
"serversManagerTitleShort": "Servidores",
|
||||
"serversManagerTitleLong": "Servidor que você hospeda",
|
||||
"saveServerButton": "Salvar servidor",
|
||||
"serverAutostartDescription": "Controla se o aplicativo iniciará automaticamente o servidor no início",
|
||||
"serverAutostartLabel": "Autoinício",
|
||||
"serverEnabledDescription": "Iniciar ou parar servidor",
|
||||
"serverEnabled": "Servidor ativado",
|
||||
"serverDescriptionDescription": "Sua descrição do servidor é apenas para uso de gerenciamento pessoal, nunca será compartilhada",
|
||||
"serverDescriptionLabel": "Descrição do servidor",
|
||||
"serverAddress": "Endereço do servidor",
|
||||
"editServerTitle": "Editar servidor",
|
||||
"addServerTitle": "Adicionar servidor",
|
||||
"titleManageProfilesShort": "Perfis",
|
||||
"descriptionFileSharing": "O experimento de compartilhamento de arquivos permite enviar e receber arquivos de contatos e grupos do Cwtch. Note que compartilhar um arquivo com um grupo resultará em membros desse grupo conectando-se diretamente com você através do Cwtch para baixá-lo.",
|
||||
"settingFileSharing": "Compartilhar arquivo",
|
||||
"tooltipSendFile": "Mandar arquivo",
|
||||
"messageFileOffered": "Contato está oferecendo mandar um arquivo para você",
|
||||
"messageFileSent": "Você mandou um arquivo",
|
||||
"messageEnableFileSharing": "Habilite o experimento de compartilhamento de arquivos para visualizar esta mensagem.",
|
||||
"labelFilesize": "Tamanho",
|
||||
"labelFilename": "Nome do arquivo",
|
||||
"downloadFileButton": "Download",
|
||||
"openFolderButton": "Abrir pasta",
|
||||
"retrievingManifestMessage": "Obtendo informações do arquivo...",
|
||||
"descriptionStreamerMode": "Se ativada, esta opção torna o aplicativo mais privado visualmente para streaming ou apresentação com, por exemplo, perfil e endereços de contato ocultos.",
|
||||
"streamerModeLabel": "Streamer\/Modo de apresentação",
|
||||
"archiveConversation": "Arquivar esta conversa",
|
||||
"blockUnknownConnectionsEnabledDescription": "As conexões de contatos desconhecidos são bloqueadas. Você pode mudar isto em Configurações",
|
||||
"showMessageButton": "Mostrar mensagem",
|
||||
"blockedMessageMessage": "Esta mensagem é de um perfil que você bloqueou.",
|
||||
"placeholderEnterMessage": "Digitar a mensagem...",
|
||||
"plainProfileDescription": "Recomendamos que você proteja seus perfis Cwtch com uma senha. Se você não definir uma senha neste perfil, qualquer pessoa que tenha acesso a este dispositivo poderá acessar informações sobre este perfil, incluindo contatos, mensagens e chaves criptográficas sensíveis.",
|
||||
"encryptedProfileDescription": "Criptografar um perfil com uma senha o protege de outras pessoas que também podem usar este dispositivo. Os perfis criptografados não podem ser descriptografados, exibidos ou acessados até que a senha correta seja inserida para desbloqueá-los.",
|
||||
"addContactConfirm": "Adicionar contato %1",
|
||||
"addContact": "Adicionar contato",
|
||||
"contactGoto": "Vá para conversa com %1",
|
||||
"settingUIColumnOptionSame": "O mesmo que a configuração do modo retrato",
|
||||
"settingUIColumnDouble14Ratio": "Duplo (1:4)",
|
||||
"settingUIColumnDouble12Ratio": "Duplo (1:2)",
|
||||
"settingUIColumnSingle": "Único",
|
||||
"settingUIColumnLandscape": "Colunas da interface em Modo Paisagem",
|
||||
"settingUIColumnPortrait": "Colunas da interface em Modo Retrato",
|
||||
"tooltipRemoveThisQuotedMessage": "Remover mensagem mencionada",
|
||||
"tooltipReplyToThisMessage": "Responder esta mensagem",
|
||||
"tooltipRejectContactRequest": "Rejeitar pedido de contato",
|
||||
"tooltipAcceptContactRequest": "Aceitar pedido de contato",
|
||||
"notificationNewMessageFromGroup": "Nova mensagem de um grupo!",
|
||||
"notificationNewMessageFromPeer": "Nova mensagem de um contato!",
|
||||
"tooltipHidePassword": "Ocultar Senha",
|
||||
"tooltipShowPassword": "Mostrar Senha",
|
||||
"groupInviteSettingsWarning": "Você foi convidado a fazer parte de um grupo! Por favor, habilite a Experiência de Bate-papo em Grupo em Configurações para ver este convite.",
|
||||
"shutdownCwtchAction": "Desligar Cwtch",
|
||||
"shutdownCwtchDialog": "Você tem certeza que quer encerrar o Cwtch? Isto fechará todas as conexões, e sairá do aplicativo.",
|
||||
"shutdownCwtchDialogTitle": "Desligar o Cwtch?",
|
||||
"shutdownCwtchTooltip": "Desligar o Cwtch",
|
||||
"malformedMessage": "Mensagem malformada",
|
||||
"profileDeleteSuccess": "Perfil deletado com sucesso",
|
||||
"debugLog": "Ative o console de depuração de logs",
|
||||
"torNetworkStatus": "Status da rede Tor",
|
||||
"addContactFirst": "Adicione ou escolha um contato para começar a conversar.",
|
||||
"createProfileToBegin": "Por favor, crie ou desbloqueie um perfil para começar",
|
||||
"nickChangeSuccess": "Nome do perfil alterado com sucesso",
|
||||
"addServerFirst": "Você precisa adicionar um servidor antes de poder criar um grupo",
|
||||
"deleteProfileSuccess": "Perfil deletado com sucesso!",
|
||||
"sendInvite": "Mandar um contato ou convite de grupo",
|
||||
"sendMessage": "Mandar mensagem",
|
||||
"cancel": "Cancelar",
|
||||
"resetTor": "Resetar",
|
||||
"torStatus": "Tor Status",
|
||||
"torVersion": "Versão do Tor",
|
||||
"sendAnInvitation": "Você enviou um convite para: ",
|
||||
"contactSuggestion": "Esta é uma sugestão de contato para: ",
|
||||
"rejected": "Rejeitado!",
|
||||
"accepted": "Aceito!",
|
||||
"chatHistoryDefault": "Esta conversa será excluída quando o Cwtch for encerrado! O histórico de mensagens pode ser habilitado por conversa através do menu Configurações, no canto superior direito.",
|
||||
"newPassword": "Nova senha",
|
||||
"yesLeave": "Sim, deixa esta conversa",
|
||||
"reallyLeaveThisGroupPrompt": "Você tem certeza que quer deixar esta conversa? Todas as mensagens e atributos serão apagados.",
|
||||
"leaveConversation": "Deixar esta conversa",
|
||||
"inviteToGroup": "Você foi convidado a se juntar a um grupo:",
|
||||
"titleManageServers": "Gerenciar Servidores",
|
||||
"successfullAddedContact": "Adicionado com sucesso ",
|
||||
"descriptionBlockUnknownConnections": "Se ativada, esta opção fechará automaticamente as conexões dos usuários Cwtch que não tenham sido adicionadas à sua lista de contatos.",
|
||||
"descriptionExperimentsGroups": "O experimento de grupo permite ao Cwtch conectar-se com uma infra-estrutura de servidor não confiável para facilitar a comunicação com mais de um contato.",
|
||||
"descriptionExperiments": "Os experimentos Cwtch são opcionais, características opt-in que adicionam funcionalidades adicionais ao Cwtch que podem ter considerações de privacidade diferentes das tradicionais conversas resistentes a metadados 1:1, por exemplo, conversas em grupo, integração de bot, etc.",
|
||||
"titleManageProfiles": "Gerencie perfis do Cwtch",
|
||||
"tooltipUnlockProfiles": "Desbloqueie perfis criptografados digitando sua senha.",
|
||||
"titleManageContacts": "Conversas",
|
||||
"tooltipAddContact": "Adicionar novo contato a conversa",
|
||||
"tooltipOpenSettings": "Abra o painel de ajustes",
|
||||
"contactAlreadyExists": "Contato já existe",
|
||||
"invalidImportString": "Importação de string inválido",
|
||||
"conversationSettings": "Configurações da Conversa",
|
||||
"enterCurrentPasswordForDelete": "Por favor entre sua senha atual para deletar este perfil.",
|
||||
"enableGroups": "Ativar chat em grupo",
|
||||
"todoPlaceholder": "Afazer…",
|
||||
"addNewItem": "Adicionar novo item à lista",
|
||||
"addListItem": "Adicionar Item à Lista",
|
||||
"newConnectionPaneTitle": "Nova Conexão",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusConnecting": "Conectando à rede e contatos...",
|
||||
"networkStatusAttemptingTor": "Tentando conectar à rede Tor",
|
||||
"networkStatusDisconnected": "Desconectado da internet, confira a sua conexão",
|
||||
"viewGroupMembershipTooltip": "Ver Participação em Grupo",
|
||||
"loadingTor": "Carregando Tor...",
|
||||
"smallTextLabel": "Pequeno",
|
||||
"defaultScalingText": "Texto tamanho padrão (fator de escala: ",
|
||||
"builddate": "Construído em: %2",
|
||||
"version": "Versão %1",
|
||||
"versionTor": "Versão %1 com Tor %2",
|
||||
"experimentsEnabled": "Ativar experimentos",
|
||||
"themeDark": "Escuro",
|
||||
"themeLight": "Claro",
|
||||
"settingTheme": "Use temas claros",
|
||||
"largeTextLabel": "Grande",
|
||||
"settingInterfaceZoom": "Nível de zoom",
|
||||
"settingLanguage": "Linguagem",
|
||||
"blockUnknownLabel": "Bloquear Contatos Desconhecidos",
|
||||
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)",
|
||||
"versionBuilddate": "Versão: %1 Construído em: %2",
|
||||
"cwtchSettingsTitle": "Configurações do Cwtch",
|
||||
"unlock": "Desbloquear",
|
||||
"yourServers": "Seus servidores",
|
||||
"yourProfiles": "Seus perfis",
|
||||
"error0ProfilesLoadedForPassword": "0 perfis carregados com esta senha",
|
||||
"password": "Senha",
|
||||
"enterProfilePassword": "Coloque uma senha para ver seus perfis",
|
||||
"addNewProfileBtn": "Adicionar novo perfil",
|
||||
"deleteConfirmText": "DELETE",
|
||||
"deleteProfileConfirmBtn": "Realmente deletar o perfil",
|
||||
"deleteConfirmLabel": "digite DELETE para confirmar",
|
||||
"deleteProfileBtn": "Deletar Perfil",
|
||||
"passwordChangeError": "Erro ao mudar a senha: senha fornecida rejeitada",
|
||||
"passwordErrorMatch": "Senhas não são iguais",
|
||||
"saveProfileBtn": "Salvar Perfil",
|
||||
"createProfileBtn": "Criar Perfil",
|
||||
"passwordErrorEmpty": "Senha não pode ser vazia",
|
||||
"password2Label": "Recoloque a senha",
|
||||
"password1Label": "Senha",
|
||||
"currentPasswordLabel": "Senha Atual",
|
||||
"yourDisplayName": "Seu nome de exibição",
|
||||
"profileOnionLabel": "Envie este endereço para contatos que você deseja se conectar",
|
||||
"noPasswordWarning": "Não usar uma senha nesta conta significa que todos os dados armazenados localmente não serão criptografados",
|
||||
"radioNoPassword": "Descriptografado (Sem senha)",
|
||||
"radioUsePassword": "Senha",
|
||||
"editProfile": "Editar Perfil",
|
||||
"newProfile": "Novo Perfil",
|
||||
"defaultProfileName": "Alice",
|
||||
"profileName": "Nome de exibição",
|
||||
"editProfileTitle": "Editar perfil",
|
||||
"addProfileTitle": "Adicionar novo perfil",
|
||||
"deleteBtn": "Deletar",
|
||||
"unblockBtn": "Desbloquear Contato",
|
||||
"dontSavePeerHistory": "Deletar histórico",
|
||||
"savePeerHistoryDescription": "Determina se deve excluir algum histórico associado com o contato.",
|
||||
"savePeerHistory": "Salvar histórico",
|
||||
"blockBtn": "Bloquear Contato",
|
||||
"saveBtn": "Salvar",
|
||||
"displayNameLabel": "Nome de Exibição",
|
||||
"copiedToClipboardNotification": "Copiado",
|
||||
"addressLabel": "Endereço",
|
||||
"puzzleGameBtn": "Jogo de Adivinhação",
|
||||
"bulletinsBtn": "Boletins",
|
||||
"listsBtn": "Listas",
|
||||
"chatBtn": "Chat",
|
||||
"rejectGroupBtn": "Recusar",
|
||||
"acceptGroupBtn": "Aceitar",
|
||||
"acceptGroupInviteLabel": "Você quer aceitar o convite para",
|
||||
"newGroupBtn": "Criar novo grupo",
|
||||
"copyBtn": "Copiar",
|
||||
"peerOfflineMessage": "Contato está off-line, mensagens não podem ser entregues agora.",
|
||||
"peerBlockedMessage": "Contato está bloquado",
|
||||
"pendingLabel": "Pendente",
|
||||
"acknowledgedLabel": "Confirmada",
|
||||
"couldNotSendMsgError": "Não deu para enviar esta mensagem",
|
||||
"dmTooltip": "Clique para DM",
|
||||
"membershipDescription": "A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo.",
|
||||
"addListItemBtn": "Adicionar Item",
|
||||
"peerNotOnline": "Contato está off-line. Não pode receber mensagens agora.",
|
||||
"searchList": "Lista de Pesquisa",
|
||||
"update": "Atualizar",
|
||||
"inviteBtn": "Convidar",
|
||||
"inviteToGroupLabel": "Convidar ao grupo",
|
||||
"groupNameLabel": "Nome do Grupo",
|
||||
"viewServerInfo": "Server Info",
|
||||
"serverNotSynced": "Sincronizando novas mensagens (Isto pode levar um tempo)...",
|
||||
"serverSynced": "Sincronizado",
|
||||
"serverConnectivityDisconnected": "Servidor Desconectado",
|
||||
"serverConnectivityConnected": "Servidor Conectado",
|
||||
"serverInfo": "Informação do Servidor",
|
||||
"invitationLabel": "Convite",
|
||||
"serverLabel": "Servidor",
|
||||
"search": "Pesquisar...",
|
||||
"blocked": "Bloqueado",
|
||||
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
|
||||
"titlePlaceholder": "título…",
|
||||
"postNewBulletinLabel": "Postar novo boletim",
|
||||
"newBulletinLabel": "Novo Boletim",
|
||||
"joinGroup": "Entrar no grupo",
|
||||
"createGroup": "Criar grupo",
|
||||
"addPeer": "Adicionar Contato",
|
||||
"groupAddr": "Endereços",
|
||||
"invitation": "Convite",
|
||||
"server": "Servidor",
|
||||
"peerName": "Nome",
|
||||
"peerAddress": "Endereço",
|
||||
"joinGroupTab": "Juntar-se a um grupo",
|
||||
"createGroupTab": "Criar um grupo",
|
||||
"addPeerTab": "Adicionar um contato",
|
||||
"createGroupBtn": "Criar",
|
||||
"defaultGroupName": "Grupo incrível",
|
||||
"createGroupTitle": "Criar Grupo"
|
||||
}
|
|
@ -1,15 +1,69 @@
|
|||
{
|
||||
"@@locale": "ro",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"deleteProfileConfirmBtn": "Sigur ștergeti profilul",
|
||||
"deleteConfirmLabel": "Tastați ȘTERGE pentru a confirma",
|
||||
"localeDa": "Daneză",
|
||||
"localeCy": "Velşă",
|
||||
"conversationNotificationPolicySettingDescription": "Controlați comportamentul de notificare al acestei conversații",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Turcă \/ Türk",
|
||||
"localeIt": "Italiană",
|
||||
"localeEn": "Engleză \/ English",
|
||||
"localeFr": "Franceză \/ Français",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"localeDe": "Germană",
|
||||
"localeLb": "Luxemburgheză",
|
||||
"localeNo": "Norvegiană",
|
||||
"localeEl": "Greacă",
|
||||
"localeLb": "Luxemburgheză",
|
||||
"localePl": "Poloneză",
|
||||
"localeRo": "Română",
|
||||
"localeRU": "Rusă",
|
||||
"localeEs": "Spaniolă",
|
||||
"localeDa": "Daneză",
|
||||
"localePt": "Portugheză",
|
||||
"localeCy": "Velşă",
|
||||
"settingImagePreviewsDescription": "Imaginile vor fi descărcate și previzualizate automat. Vă rugăm să rețineți că previzualizările imaginilor pot duce adesea la vulnerabilități de securitate și nu ar trebui să activați acest Experiment dacă utilizați Cwtch cu contacte care nu sunt de încredere. Imaginile de profil sunt planificate pentru Cwtch 1.6.",
|
||||
"tooltipPreviewFormatting": "Preview Message Formatting",
|
||||
"tooltipCode": "Code \/ Monospace",
|
||||
"tooltipStrikethrough": "Strikethrough",
|
||||
"tooltipSubscript": "Subscript",
|
||||
"tooltipSuperscript": "Superscript",
|
||||
"tooltipItalicize": "Italic",
|
||||
"tooltipBackToMessageEditing": "Back to Message Editing",
|
||||
"tooltipBoldText": "Bold",
|
||||
"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",
|
||||
"deleteProfileConfirmBtn": "Sigur ștergeti profilul",
|
||||
"deleteConfirmLabel": "Tastați ȘTERGE pentru a confirma",
|
||||
"conversationNotificationPolicySettingDescription": "Controlați comportamentul de notificare al acestei conversații",
|
||||
"createGroupTitle": "Creați un grup",
|
||||
"serverLabel": "Server",
|
||||
"defaultGroupName": "Grup minunat",
|
||||
|
@ -105,10 +159,6 @@
|
|||
"zoomLabel": "Zoomul interfeței (afectează dimensiunile textului și ale butoanelor)",
|
||||
"blockUnknownLabel": "Blocați contactele necunoscute",
|
||||
"settingLanguage": "Limba",
|
||||
"localeEn": "Engleză",
|
||||
"localeFr": "Franceză",
|
||||
"localePt": "Portugheză",
|
||||
"localeDe": "Germană",
|
||||
"settingInterfaceZoom": "Nivelul de zoom",
|
||||
"largeTextLabel": "Mare",
|
||||
"settingTheme": "Utilizați teme luminoase",
|
||||
|
@ -129,8 +179,6 @@
|
|||
"addListItem": "Adăugați o nouă listă",
|
||||
"addNewItem": "Adăugați un nou element în listă",
|
||||
"todoPlaceholder": "În construcție...",
|
||||
"localeEs": "Spaniolă",
|
||||
"localeIt": "Italiană",
|
||||
"enableGroups": "Activează chatul de grup",
|
||||
"enterCurrentPasswordForDelete": "Vă rugăm să introduceți parola actuală pentru a șterge acest profil.",
|
||||
"conversationSettings": "Setările conversației",
|
||||
|
@ -184,7 +232,6 @@
|
|||
"tooltipRejectContactRequest": "Respingeți această cerere de contact",
|
||||
"tooltipReplyToThisMessage": "Răspundeți la acest mesaj",
|
||||
"tooltipRemoveThisQuotedMessage": "Eliminați mesajul citat.",
|
||||
"localePl": "Poloneză",
|
||||
"settingUIColumnPortrait": "Coloane UI în modul Portret",
|
||||
"settingUIColumnLandscape": "Coloane UI în modul Peisaj",
|
||||
"settingUIColumnSingle": "Singur",
|
||||
|
@ -244,7 +291,6 @@
|
|||
"fileCheckingStatus": "Se verifică starea descărcării",
|
||||
"verfiyResumeButton": "Verificați\/reluați",
|
||||
"copyServerKeys": "Copiați cheile",
|
||||
"localeRU": "Rusă",
|
||||
"newMessagesLabel": "Mesaje noi",
|
||||
"importLocalServerLabel": "Importați un server găzduit local",
|
||||
"importLocalServerSelectText": "Selectați Server local",
|
||||
|
@ -261,7 +307,6 @@
|
|||
"enableExperimentClickableLinks": "Activați linkurile pe care se poate da clic",
|
||||
"experimentClickableLinksDescription": "Experimentul cu linkuri pe care se poate da clic vă permite să faceți clic pe adresele URL partajate în mesaje",
|
||||
"settingImagePreviews": "Previzualizări de imagini și poze de profil",
|
||||
"settingImagePreviewsDescription": "Imaginile vor fi descărcate și previzualizate automat. Vă rugăm să rețineți că previzualizările imaginilor pot duce adesea la vulnerabilități de securitate și nu ar trebui să activați acest Experiment dacă utilizați Cwtch cu contacte care nu sunt de încredere. Imaginile de profil sunt planificate pentru Cwtch 1.6.",
|
||||
"settingDownloadFolder": "Fișier de descărcări",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"themeNameWitch": "Vrăjitoare",
|
||||
|
|
|
@ -1,72 +1,144 @@
|
|||
{
|
||||
"@@locale": "ru",
|
||||
"@@last_modified": "2022-03-04T17:33:01+01:00",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"localeNl": "Dutch \/ Dutch",
|
||||
"experimentQRCodeDescription": "QR Code support allows sharing data (such as profile identity) by QR Codes",
|
||||
"enableExperimentQRCode": "QR Codes",
|
||||
"shareMenuQRCode": "Show QR Code",
|
||||
"shareProfileMenuTooltop": "Share profile via...",
|
||||
"acquiredTicketsFromServer": "Antispam Challenge Complete",
|
||||
"acquiringTicketsFromServer": "Performing Antispam Challenge",
|
||||
"errorDownloadDirectoryDoesNotExist": "Filesharing cannot be enabled because the Download Folder has not been set, or is set to a folder that does not exist.",
|
||||
"localeTr": "Турецкий \/ Türk",
|
||||
"localeIt": "Итальянский \/ Italiano",
|
||||
"tooltipUnpinConversation": "Unpin conversation from the top of \"Conversations\"",
|
||||
"tooltipPinConversation": "Pin conversation to the top of \"Conversations\"",
|
||||
"replyingTo": "Replying to %1",
|
||||
"fileDownloadUnavailable": "This file appears unavailable for download. The sender may have disabled downloads for this file.",
|
||||
"messageNoReplies": "There are no replies to this message.",
|
||||
"headingReplies": "Replies",
|
||||
"viewReplies": "View replies to this message",
|
||||
"restartFileShare": "Start Sharing File",
|
||||
"stopSharingFile": "Stop Sharing File",
|
||||
"manageSharedFiles": "Manage Shared Files",
|
||||
"exportProfile": "Экспорт профиля",
|
||||
"notificationContentContactInfo": "Показать текст сообщения",
|
||||
"notificationContentSimpleEvent": "Без подробностей",
|
||||
"settingsGroupExperiments": "ЭКСПЕРИМЕНТЫ",
|
||||
"settingsGroupAppearance": "НАСТРОЙКИ ОТОБРАЖЕНИЯ",
|
||||
"settingGroupBehaviour": "ПОВЕДЕНИЕ",
|
||||
"notificationContentSettingDescription": "Управление уведомлениями чатов",
|
||||
"conversationNotificationPolicyNever": "Отключить",
|
||||
"notificationPolicyDefaultAll": "По-умолчанию",
|
||||
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch, внутри сети Tor",
|
||||
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1.",
|
||||
"profileOnionLabel": "Send this address to contacts you want to connect with",
|
||||
"localeDe": "Немецкий \/ Deutsch",
|
||||
"localeDa": "Датский язык \/ Dansk",
|
||||
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами.",
|
||||
"localePt": "Португальский язык \/ Portuguesa",
|
||||
"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.",
|
||||
"conversationNotificationPolicySettingDescription": "Настройка уведомлений",
|
||||
"conversationNotificationPolicySettingLabel": "Настройка уведомлений",
|
||||
"notificationPolicySettingDescription": "Настройка уведомлений по-умолчанию",
|
||||
"notificationContentSettingLabel": "Содержимое уведомления",
|
||||
"notificationPolicySettingLabel": "Уведомления",
|
||||
"conversationNotificationPolicyOptIn": "Включить",
|
||||
"conversationNotificationPolicyDefault": "По-умолчанию",
|
||||
"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": "Состояние сервера",
|
||||
"settingUIColumnOptionSame": "Как в портретном режиме",
|
||||
"settingUIColumnSingle": "Один столбец",
|
||||
"createProfileToBegin": "Пожалуйста, создайте или импортируйте профиль, чтобы начать",
|
||||
"yesLeave": "Удалить",
|
||||
"leaveConversation": "Удалить",
|
||||
"enableGroups": "Групповые чаты",
|
||||
"settingTheme": "Ночной режим",
|
||||
"addNewProfileBtn": "Создать новый профиль",
|
||||
"savePeerHistoryDescription": "Определяет политику хранения или удаления сообщений с данным контактом",
|
||||
"savePeerHistory": "Настройка истории",
|
||||
"deleteConfirmLabel": "Введите УДАЛИТЬ, чтобы продолжить",
|
||||
"deleteConfirmText": "УДАЛИТЬ",
|
||||
"localeCy": "Валлийский",
|
||||
"localeDa": "Датский",
|
||||
"localeEl": "Греческий",
|
||||
"localeNo": "Норвежский",
|
||||
"localeLb": "Люксембургский",
|
||||
"settingsGroupAppearance": "Появление",
|
||||
"settingGroupBehaviour": "Поведение",
|
||||
"settingsGroupExperiments": "Эксперименты",
|
||||
"labelTorNetwork": "Сеть Tor",
|
||||
"notificationPolicyMute": "Тишина",
|
||||
"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..",
|
||||
"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": "Полночь",
|
||||
|
@ -76,7 +148,6 @@
|
|||
"themeNameVampire": "Вампир",
|
||||
"themeNameWitch": "Ведьма",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами. Аватары профиля запланированы для версии Cwtch 1.6.",
|
||||
"settingImagePreviews": "Предпросмотр изображений и фотографий профиля",
|
||||
"experimentClickableLinksDescription": "Экспериментальная функция которая позволяет нажимать на URL адреса в сообщениях",
|
||||
"enableExperimentClickableLinks": "Включить кликабельные ссылки",
|
||||
|
@ -89,11 +160,7 @@
|
|||
"groupsOnThisServerLabel": "Группы, в которых я нахожусь, размещены на этом сервере",
|
||||
"importLocalServerButton": "Импорт %1",
|
||||
"importLocalServerSelectText": "Выбрать локальный сервер",
|
||||
"importLocalServerLabel": "Импортировать локальный сервер",
|
||||
"newMessagesLabel": "Новое сообщение",
|
||||
"localeRU": "Русский",
|
||||
"profileOnionLabel": "Send this address to contacts you want to connect with",
|
||||
"savePeerHistory": "Хранить историю",
|
||||
"saveBtn": "Сохранить",
|
||||
"networkStatusOnline": "В сети",
|
||||
"defaultProfileName": "Алиса",
|
||||
|
@ -109,29 +176,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": "Контакт предлагает загрузить вам файл",
|
||||
|
@ -153,10 +214,8 @@
|
|||
"addContactConfirm": "Добавить контакт %1",
|
||||
"addContact": "Добавить контакт",
|
||||
"contactGoto": "Перейти к сообщению от %1",
|
||||
"settingUIColumnOptionSame": "Как в настройках портретного режима",
|
||||
"settingUIColumnDouble14Ratio": "Двойной (1:4)",
|
||||
"settingUIColumnDouble12Ratio": "Двойной (1:2)",
|
||||
"localePl": "Польский",
|
||||
"tooltipRemoveThisQuotedMessage": "Удалить цитируемое сообщение.",
|
||||
"tooltipReplyToThisMessage": "Ответить на это сообщение",
|
||||
"tooltipRejectContactRequest": "Отклонить запрос в контакты.",
|
||||
|
@ -175,7 +234,6 @@
|
|||
"debugLog": "Влючить отладку через консоль",
|
||||
"torNetworkStatus": "Статус сети Tor",
|
||||
"addContactFirst": "Добавьте или выберите контакт, чтобы начать чат.",
|
||||
"createProfileToBegin": "Пожалуйста, создайте или разблокируйте профиль, чтобы начать",
|
||||
"nickChangeSuccess": "Имя профиля успешно изменено",
|
||||
"addServerFirst": "Перед созданием группы, необходимо создать сервер",
|
||||
"deleteProfileSuccess": "Профиль успешно удален",
|
||||
|
@ -190,9 +248,7 @@
|
|||
"accepted": "Принять!",
|
||||
"chatHistoryDefault": "Этот чат будет удален после закрытия Cwtch! Историю сообщений можно включить для каждого чата отдельно через меню настроек в правом верхнем углу..",
|
||||
"newPassword": "Новый пароль",
|
||||
"yesLeave": "Да, оставить этот чат",
|
||||
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
|
||||
"leaveConversation": "Да, оставить этот чат",
|
||||
"inviteToGroup": "Вас пригласили присоединиться к группе:",
|
||||
"titleManageServers": "Управление серверами",
|
||||
"successfullAddedContact": "Успешно добавлен",
|
||||
|
@ -205,9 +261,6 @@
|
|||
"invalidImportString": "Недействительная строка импорта",
|
||||
"conversationSettings": "Настройки чата",
|
||||
"enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.",
|
||||
"enableGroups": "Включить Групповые чаты",
|
||||
"localeIt": "Итальянский",
|
||||
"localeEs": "Испанский",
|
||||
"todoPlaceholder": "Выполняю...",
|
||||
"addNewItem": "Добавить новый элемент в список",
|
||||
"addListItem": "Добавить новый элемент",
|
||||
|
@ -225,13 +278,8 @@
|
|||
"experimentsEnabled": "Включить Экспериментальные функции",
|
||||
"themeDark": "Темная",
|
||||
"themeLight": "Светлая",
|
||||
"settingTheme": "Тема",
|
||||
"largeTextLabel": "Большой",
|
||||
"settingInterfaceZoom": "Уровень масштабирования",
|
||||
"localeDe": "Немецкий",
|
||||
"localePt": "Португальский",
|
||||
"localeFr": "Французский",
|
||||
"localeEn": "Английский",
|
||||
"blockUnknownLabel": "Блокировать неизвестные контакты",
|
||||
"zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)",
|
||||
"versionBuilddate": "Версия: %1 Сборка от: %2",
|
||||
|
@ -242,7 +290,6 @@
|
|||
"error0ProfilesLoadedForPassword": "0 профилей, загруженных с этим паролем",
|
||||
"password": "Пароль",
|
||||
"enterProfilePassword": "Введите пароль для просмотра ваших профилей",
|
||||
"addNewProfileBtn": "Добавить новый профиль",
|
||||
"deleteProfileConfirmBtn": "Действительно удалить профиль?",
|
||||
"deleteProfileBtn": "Удалить профиль",
|
||||
"passwordChangeError": "Ошибка при смене пароля: Введенный пароль отклонен",
|
||||
|
@ -257,13 +304,11 @@
|
|||
"noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы",
|
||||
"radioNoPassword": "Незашифрованный (без пароля)",
|
||||
"radioUsePassword": "Пароль",
|
||||
"editProfile": "Изменить профиль",
|
||||
"newProfile": "Новый профиль",
|
||||
"editProfileTitle": "Изменить профиль",
|
||||
"addProfileTitle": "Добавить новый профиль",
|
||||
"unblockBtn": "Разблокировать контакт",
|
||||
"dontSavePeerHistory": "Удалить историю",
|
||||
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
|
||||
"blockBtn": "Заблокировать контакт",
|
||||
"displayNameLabel": "Отображаемое имя",
|
||||
"addressLabel": "Адрес",
|
||||
|
|
|
@ -0,0 +1,362 @@
|
|||
{
|
||||
"@@locale": "tr",
|
||||
"@@last_modified": "2022-09-15T03:18:37+02:00",
|
||||
"experimentQRCodeDescription": "QR Kod desteği profil kimliği gibi verilerin QR Kodla paylaşılmasına olanak tanır",
|
||||
"acquiringTicketsFromServer": "Spame Karşı Sınama",
|
||||
"acquiredTicketsFromServer": "Spame Karşı Sınama Tamamlandı",
|
||||
"shareProfileMenuTooltop": "Profili paylaş...",
|
||||
"shareMenuQRCode": "QR Kodunu Göster",
|
||||
"enableExperimentQRCode": "QR Kodları",
|
||||
"localeNl": "Flemenkçe \/ Dutch",
|
||||
"errorDownloadDirectoryDoesNotExist": "İndirilenler Klasörü ayarlanmadığı veya mevcut olmayan bir klasöre ayarlandığı için dosya paylaşımı etkinleştirilemiyor.",
|
||||
"radioNoPassword": "Şifrelenmemiş (Parola yok)",
|
||||
"msgAddToAccept": "Dosyayı kabul etmek için bu hesabı kişilerinize ekleyin.",
|
||||
"fileSharingSettingsDownloadFolderDescription": "Dosyalar (örn. görsel önizlemeleri etkinleştirildiğinde görsel dosyaları) otomatik olarak indirildiğinde, dosyaların indirileceği varsayılan bir konum gereklidir.",
|
||||
"torSettingsEnabledCacheDescription": "İndirilmiş Tor uzlaşmasını Cwtch'un bir sonraki açılışında yeniden kullanmak için önbelleğe alın. Bu Tor'un daha hızlı açılmasını sağlar. Devre dışı bırakıldığında Cwtch açılırken önbelleğe alınmış verileri siler.",
|
||||
"notificationContentSimpleEvent": "Yalın Bildiri",
|
||||
"exportProfileTooltip": "Bu profili şifrelenmiş bir dosyaya yedekle. Şifrelenmiş dosya başka bir Cwtch uygulamasına aktarılabilir.",
|
||||
"importProfileTooltip": "Başka Cwtch oluşumunda oluşturulmuş bir profili içeri aktarmak için şifrelenmiş Cwtch yedeği kullanın.",
|
||||
"clickableLinksWarning": "Bu URL'yi açmak Cwtch dışında bir uygulama başlatacak ve metadatanız teşhir olabilir veya Cwtch'un güvenliği tehlikeye girebilir. Yalnızca güvendiğiniz kişilerden gelen URL'leri açın. Devam etmek istediğinize emin misiniz?",
|
||||
"settingAndroidPowerExemptionDescription": "Opsiyonel: Android'den Cwtch'u optimize edilmiş güç yönetiminden muaf tutmasını isteyin. Bu, daha fazla pil kullanımı pahasına uygulamanın daha stabil çalışmasını sağlayacaktır.",
|
||||
"localeTr": "Türkçe \/ Türkçe",
|
||||
"defaultGroupName": "Muhteşem Grup",
|
||||
"defaultProfileName": "Alice",
|
||||
"localeDe": "Almanca \/ Deutsch",
|
||||
"localePl": "Lehçe \/ Polski",
|
||||
"localeDa": "Danca \/ Dansk",
|
||||
"acceptGroupInviteLabel": "Daveti kabul etmek istiyor musunuz",
|
||||
"pendingLabel": "Beklemede",
|
||||
"chatBtn": "Sohbet",
|
||||
"yourDisplayName": "Kullanıcı Adınız",
|
||||
"localeEn": "İngilizce \/ English",
|
||||
"localeFr": "Fransızca \/ Français",
|
||||
"localePt": "Portekizce \/ Portuguesa",
|
||||
"networkStatusAttemptingTor": "Tor ağına bağlanılıyor",
|
||||
"localeEs": "İspanyolca \/ Español",
|
||||
"localeIt": "İtalyanca \/ Italiano",
|
||||
"debugLog": "Konsol hata ayıklama kaydını başlat",
|
||||
"localeRU": "Rusça \/ Русский",
|
||||
"serverMetricsLabel": "Sunucu Bilgileri",
|
||||
"themeNameCwtch": "Cwtch",
|
||||
"themeNameWitch": "Cadı",
|
||||
"themeNameVampire": "Vampir",
|
||||
"themeNameGhost": "Hayalet",
|
||||
"themeNamePumpkin": "Balkabağı",
|
||||
"themeNameMermaid": "Denizkızı",
|
||||
"themeNameMidnight": "Gece",
|
||||
"themeNameNeon1": "Neon1",
|
||||
"themeNameNeon2": "Neon2",
|
||||
"descriptionACNCircuitInfo": "Anonim iletişim ağının (ACN) bu konuşmaya bağlanmak için kullandığı yol hakkında ayrıntılı bilgi.",
|
||||
"tooltipSelectACustomProfileImage": "Profil Resmi Seçin",
|
||||
"notificationPolicyMute": "Sessiz",
|
||||
"localeRo": "Romence \/ Română",
|
||||
"localeLb": "Lüksemburgca \/ Lëtzebuergesch",
|
||||
"localeNo": "Norveççe \/ Norsk",
|
||||
"localeEl": "Yunanca \/ Ελληνικά",
|
||||
"localeCy": "Galce \/ Cymraeg",
|
||||
"notificationPolicyOptIn": "Mümkünse Al",
|
||||
"conversationNotificationPolicyOptIn": "Mümkünse Al",
|
||||
"tooltipCode": "Kod \/ Monospace",
|
||||
"createGroupTitle": "Grup Oluştur",
|
||||
"serverLabel": "Sunucu",
|
||||
"createGroupBtn": "Oluştur",
|
||||
"profileOnionLabel": "Bu adresi bağlantı kurmak istediğiniz insanlara gönderin",
|
||||
"addPeerTab": "Kişi ekle",
|
||||
"createGroupTab": "Grup oluştur",
|
||||
"joinGroupTab": "Gruba katıl",
|
||||
"peerAddress": "Adres",
|
||||
"peerName": "İsim",
|
||||
"server": "Sunucu",
|
||||
"invitation": "Davet",
|
||||
"groupAddr": "Adres",
|
||||
"addPeer": "Kişi Ekle",
|
||||
"createGroup": "Grup oluştur",
|
||||
"joinGroup": "Gruba katıl",
|
||||
"newBulletinLabel": "Yeni Bülten",
|
||||
"postNewBulletinLabel": "Yeni bülten yayınla",
|
||||
"titlePlaceholder": "başlık...",
|
||||
"pasteAddressToAddContact": "Yeni konuşma eklemek için buraya bir cwtch adresi, davetiye veya anahtar paketi yapıştırın",
|
||||
"blocked": "Engelli",
|
||||
"search": "Ara...",
|
||||
"invitationLabel": "Davet",
|
||||
"serverInfo": "Sunucu Bilgileri",
|
||||
"serverConnectivityConnected": "Sunucu Bağlandı",
|
||||
"serverConnectivityDisconnected": "Sunucu Bağlantısı Kesildi",
|
||||
"serverSynced": "Senkronize Edildi",
|
||||
"serverNotSynced": "Yeni Mesajlar Senkronize Oluyor (Biraz zaman alabilir)...",
|
||||
"viewServerInfo": "Sunucu Bilgileri",
|
||||
"groupNameLabel": "Grup İsmi",
|
||||
"saveBtn": "Kaydet",
|
||||
"inviteToGroupLabel": "Gruba davet et",
|
||||
"inviteBtn": "Davet Et",
|
||||
"deleteBtn": "Sil",
|
||||
"update": "Güncelleştir",
|
||||
"searchList": "Listede Ara",
|
||||
"peerNotOnline": "Kişi çevrimdışı. Uygulamalar şu anda kullanılamıyor.",
|
||||
"addListItemBtn": "Öğe Ekle",
|
||||
"membershipDescription": "Aşağıda gruba ileti gönderen kullanıcıların listesi verilmiştir. Bu liste gruba erişimi olan tüm kullanıcıları yansıtmayabilir.",
|
||||
"dmTooltip": "DM için tıklayın",
|
||||
"couldNotSendMsgError": "Mesaj gönderilemedi",
|
||||
"acknowledgedLabel": "Onaylandı",
|
||||
"peerBlockedMessage": "Kişi engelli",
|
||||
"peerOfflineMessage": "Kişi çevrimdışı, mesajlar şu anda iletilemiyor",
|
||||
"copyBtn": "Kopyala",
|
||||
"newGroupBtn": "Yeni grup oluştur",
|
||||
"acceptGroupBtn": "Kabul Et",
|
||||
"rejectGroupBtn": "Reddet",
|
||||
"listsBtn": "Listeler",
|
||||
"bulletinsBtn": "Bültenler",
|
||||
"puzzleGameBtn": "Yapboz",
|
||||
"addressLabel": "Adres",
|
||||
"copiedToClipboardNotification": "Panoya kopyalandı",
|
||||
"displayNameLabel": "Lütfen bir ad girin",
|
||||
"blockBtn": "Kişiyi Engelle",
|
||||
"savePeerHistory": "Geçmişi Kaydet",
|
||||
"savePeerHistoryDescription": "Kişi ile ilgili tüm geçmişin silinip silinmeyeceğini belirler.",
|
||||
"dontSavePeerHistory": "Geçmişi Sil",
|
||||
"unblockBtn": "Kişi Engelini Kaldır",
|
||||
"addProfileTitle": "Yeni profil ekle",
|
||||
"editProfileTitle": "Profili Düzenle",
|
||||
"profileName": "Kullanıcı adı",
|
||||
"newProfile": "Yeni Profil",
|
||||
"editProfile": "Profili Düzenle",
|
||||
"noPasswordWarning": "Bu hesapta parola kullanılmaması yerel olarak depolanan verilerin şifrelenmeyeceği anlamına gelir",
|
||||
"currentPasswordLabel": "Mevcut Parola",
|
||||
"password2Label": "Parolayı yeniden gir",
|
||||
"passwordErrorEmpty": "Parola boş bırakılamaz",
|
||||
"createProfileBtn": "Profil Oluştur",
|
||||
"saveProfileBtn": "Profili Kaydet",
|
||||
"passwordErrorMatch": "Parolalar eşleşmiyor",
|
||||
"passwordChangeError": "Parola değiştirilirken hata oluştu: Verilen parola reddedildi",
|
||||
"deleteProfileBtn": "Profili Sil",
|
||||
"deleteConfirmLabel": "Onaylamak için DELETE yazın",
|
||||
"deleteProfileConfirmBtn": "Profili Gerçekten Sil",
|
||||
"deleteConfirmText": "SİL",
|
||||
"addNewProfileBtn": "Yeni profil ekle",
|
||||
"enterProfilePassword": "Profillerinizi görmek için parolayı girin",
|
||||
"error0ProfilesLoadedForPassword": "Bu parolayla 0 profil yüklendi",
|
||||
"yourProfiles": "Profilleriniz",
|
||||
"yourServers": "Sunucularınız",
|
||||
"unlock": "Kilidi Aç",
|
||||
"cwtchSettingsTitle": "Cwtch Ayarları",
|
||||
"versionBuilddate": "Sürüm: %1 Derlendiği tarih: %2",
|
||||
"zoomLabel": "Arayüz yakınlaştırma (aslen metin ve buton boyutlarını etkiler)",
|
||||
"blockUnknownLabel": "Tanınmayan Kişileri Engelle",
|
||||
"settingLanguage": "Dil",
|
||||
"settingInterfaceZoom": "Yakınlaştırma seviyesi",
|
||||
"largeTextLabel": "Büyük",
|
||||
"settingTheme": "Açık Tema Kullan",
|
||||
"themeLight": "Açık",
|
||||
"themeDark": "Koyu",
|
||||
"experimentsEnabled": "Deneyleri Etkinleştir",
|
||||
"versionTor": "Sürüm %1 ve tor %2",
|
||||
"version": "Sürüm %1",
|
||||
"builddate": "Derlendiği tarih: %2",
|
||||
"defaultScalingText": "Varsayılan metin boyutu (ölçek faktörü: ",
|
||||
"smallTextLabel": "Küçük",
|
||||
"loadingTor": "Tor yükleniyor...",
|
||||
"viewGroupMembershipTooltip": "Grup Üyeliğini Görüntüle",
|
||||
"networkStatusDisconnected": "İnternet bağlantısı kesildi, bağlantınızı kontrol edin",
|
||||
"networkStatusConnecting": "Ağa ve kişilere bağlanıyor...",
|
||||
"networkStatusOnline": "Çevrimiçi",
|
||||
"newConnectionPaneTitle": "Yeni Bağlantı",
|
||||
"addListItem": "Yeni Liste Öğesi Ekle",
|
||||
"addNewItem": "Listeye yeni bir öğe ekle",
|
||||
"todoPlaceholder": "Yapılacaklar...",
|
||||
"enableGroups": "Grup Sohbetini Etkinleştir",
|
||||
"enterCurrentPasswordForDelete": "Profili silmek için parolayı girin.",
|
||||
"conversationSettings": "Sohbet Ayarları",
|
||||
"invalidImportString": "Geçersiz içe aktarma girdisi",
|
||||
"contactAlreadyExists": "Kişi Zaten Ekli",
|
||||
"tooltipOpenSettings": "Ayarlar bölmesini aç",
|
||||
"descriptionExperiments": "Cwtch deneyleri, Cwtch'a grup sohbeti, bot entegrasyonu vb. gibi 1:1 meta veriye dayanıklılıktan farklı gizlilik önceliklerine sahip olabilen işlevler ekleyen opsiyonel özelliklerdir.",
|
||||
"tooltipAddContact": "Yeni bir kişi veya sohbet ekle",
|
||||
"titleManageContacts": "Sohbetler",
|
||||
"tooltipUnlockProfiles": "Parolalarını girerek şifrelenmiş profillerin kilidini açın.",
|
||||
"titleManageProfiles": "Cwtch Profillerini Yönet",
|
||||
"descriptionExperimentsGroups": "Grup deneyi, Cwtch'un birden fazla kişiyle bağlantısını kolaylaştırmak için güvenilmeyen sunucu altyapısına bağlanmasına olanak tanır.",
|
||||
"descriptionBlockUnknownConnections": "Bu seçenek etkinse, kişi listenizde olmayan kullanıcılardan gelen bağlantılar otomatik olarak kapatılır.",
|
||||
"successfullAddedContact": "Başarıyla eklendi",
|
||||
"titleManageServers": "Sunucuları Yönet",
|
||||
"inviteToGroup": "Bir gruba katılma daveti aldınız:",
|
||||
"leaveConversation": "Sohbetten Ayrıl",
|
||||
"reallyLeaveThisGroupPrompt": "Bu görüşmeden ayrılmak istediğinize emin misiniz? Tüm mesajlar silinecek.",
|
||||
"yesLeave": "Evet, Sohbetten Ayrıl",
|
||||
"chatHistoryDefault": "Cwtch kapatıldığında bu konuşma silinecek! Mesaj geçmişi farklı sohbetler için sağ üstteki Ayarlar menüsünden etkinleştirilebilir.",
|
||||
"accepted": "Kabul edildi!",
|
||||
"rejected": "Reddedildi!",
|
||||
"contactSuggestion": "İletişim önerileri: ",
|
||||
"sendAnInvitation": "Bir davetiye gönderdiniz: ",
|
||||
"torVersion": "Tor Sürümü",
|
||||
"torStatus": "Tor Durumu",
|
||||
"resetTor": "Sıfırla",
|
||||
"cancel": "İptal",
|
||||
"sendMessage": "Mesaj Gönder",
|
||||
"sendInvite": "Kişi veya grup daveti gönderme",
|
||||
"deleteProfileSuccess": "Profil başarıyla silindi",
|
||||
"addServerFirst": "Grup oluşturmadan önce sunucu eklemeniz gerekir",
|
||||
"nickChangeSuccess": "Profil kullanıcı adı başarıyla değiştirildi",
|
||||
"createProfileToBegin": "Başlamak için bir profil oluşturun veya kilidini açın",
|
||||
"addContactFirst": "Sohbete başlamak için bir kişi ekleyin veya seçin.",
|
||||
"torNetworkStatus": "Tor ağ durumu",
|
||||
"profileDeleteSuccess": "Profil başarıyla silindi",
|
||||
"malformedMessage": "Hatalı biçimlendirilmiş mesaj",
|
||||
"shutdownCwtchTooltip": "Cwtch'u Kapat",
|
||||
"shutdownCwtchDialogTitle": "Cwtch'u Kapat?",
|
||||
"shutdownCwtchDialog": "Cwtch'i kapatmak istediğinize emin misiniz? Bu tüm bağlantıları kapatacak, ve uygulamadan çıkacaktır.",
|
||||
"shutdownCwtchAction": "Cwtch'u Kapat",
|
||||
"groupInviteSettingsWarning": "Gruba katılma daveti aldınız! Bu Daveti görüntülemek için lütfen Ayarlar'dan Grup Sohbeti Deneyini etkinleştirin.",
|
||||
"tooltipHidePassword": "Parolayı Gizle",
|
||||
"tooltipShowPassword": "Parolayı Göster",
|
||||
"newPassword": "Yeni Parola",
|
||||
"radioUsePassword": "Parola",
|
||||
"password1Label": "Parola",
|
||||
"password": "Parola",
|
||||
"notificationNewMessageFromPeer": "Yeni mesaj!",
|
||||
"notificationNewMessageFromGroup": "Yeni grup mesajı!",
|
||||
"tooltipAcceptContactRequest": "Bağlantı isteğini kabul et",
|
||||
"tooltipRejectContactRequest": "Bağlantı isteğini reddet",
|
||||
"tooltipReplyToThisMessage": "Bu mesajı yanıtla",
|
||||
"tooltipRemoveThisQuotedMessage": "Alıntılanan mesajı kaldır.",
|
||||
"settingUIColumnPortrait": "UI Sütunları Portre Modu",
|
||||
"settingUIColumnLandscape": "UI Sütunları Yatay Modu",
|
||||
"settingUIColumnSingle": "Tek",
|
||||
"settingUIColumnDouble12Ratio": "Çift (1:2)",
|
||||
"settingUIColumnDouble14Ratio": "Çift (1:4)",
|
||||
"settingUIColumnOptionSame": "Portre modu ayarı ile aynı",
|
||||
"contactGoto": "%1 ile olan sohbete git",
|
||||
"addContact": "Kişi ekle",
|
||||
"addContactConfirm": "%1 kişisini ekle",
|
||||
"encryptedProfileDescription": "Bir profili parola ile şifrelemek, profili bu aygıtı kullanabilecek diğer kişilerden korur. Doğru şifre girilene kadar şifrelenmiş profiller görüntülenemez veya erişilemez.",
|
||||
"plainProfileDescription": "Cwtch profillerinizi parola ile korumanızı öneririz. Bir parola belirlemezseniz, bu cihaza erişimi olan herkes kişiler, mesajlar ve kriptografik anahtarlar da dahil olmak üzere hassas bilgilerinize erişebilir.",
|
||||
"placeholderEnterMessage": "Bir mesaj yazın...",
|
||||
"labelFilesize": "Boyut",
|
||||
"blockedMessageMessage": "Bu mesaj blokladığınız bir profilden gönderilmiş",
|
||||
"showMessageButton": "Mesajı Görüntüle",
|
||||
"blockUnknownConnectionsEnabledDescription": "Bilinmeyen kişilerden gelen bağlantılar engelli. Bu özelliği Ayarlar'dan değiştirebilirsiniz",
|
||||
"archiveConversation": "Bu Sohbeti Arşivle",
|
||||
"streamerModeLabel": "Yayıncı\/Sunum Modu",
|
||||
"descriptionStreamerMode": "Bu seçenek etkinleştirildiğinde profiller, iletişim adresleri gibi bilgiler saklanarak uygulama daha gizli hale getirilir.",
|
||||
"retrievingManifestMessage": "Dosya bilgileri alınıyor...",
|
||||
"openFolderButton": "Klasörü Aç",
|
||||
"downloadFileButton": "İndir",
|
||||
"labelFilename": "Dosya Adı",
|
||||
"messageEnableFileSharing": "Bu mesajı görüntüleyebilmek için dosya paylaşma deneyini etkinleştirin.",
|
||||
"settingFileSharing": "Dosya Paylaşma",
|
||||
"messageFileSent": "Bir dosya gönderdiniz",
|
||||
"messageFileOffered": "Kişi size bir dosya göndermek istiyor",
|
||||
"tooltipSendFile": "Dosya Gönder",
|
||||
"descriptionFileSharing": "Dosya paylaşım deneyi, Cwtch kişileri ve gruplarından dosya gönderip almanızı sağlar. Bir dosyayı bir grupla paylaşmanın, o grubun üyelerinin dosyayı indirmek için doğrudan Cwtch üzerinden sizinle bağlantı kurmasıyla sonuçlanacağını unutmayın.",
|
||||
"titleManageProfilesShort": "Profiller",
|
||||
"addServerTitle": "Sunucu Ekle",
|
||||
"editServerTitle": "Sunucuyu Düzenle",
|
||||
"serverAddress": "Sunucu Adresi",
|
||||
"serverDescriptionLabel": "Sunucu Açıklaması",
|
||||
"serverDescriptionDescription": "Kişisel yönetiminiz için sunucu açıklaması, dışarıyla asla paylaşılmayacaktır",
|
||||
"serverEnabled": "Sunucu Etkin",
|
||||
"serverEnabledDescription": "Sunucuyu başlat veya durdur",
|
||||
"serverAutostartLabel": "Otomatik başlatma",
|
||||
"serverAutostartDescription": "Uygulamanın açılışta sunucuyu otomatik olarak başlatıp başlatmayacağını belirler",
|
||||
"saveServerButton": "Sunucuyu Kaydet",
|
||||
"serversManagerTitleLong": "Barındırdığınız Sunucular",
|
||||
"serversManagerTitleShort": "Sunucular",
|
||||
"addServerTooltip": "Yeni sunucu ekle",
|
||||
"unlockServerTip": "Başlamak için bir sunucu oluşturun veya sunucunuzun kilidini açın",
|
||||
"unlockProfileTip": "Başlamak için bir profil oluşturun veya profilinizin kilidini açın",
|
||||
"enterServerPassword": "Sunucuyu açmak için şifre girin",
|
||||
"settingServersDescription": "Sunucu barındırma deneyi Cwtch sunucularını barındırmayı ve yönetmeyi sağlar",
|
||||
"settingServers": "Sunucu Barındırma",
|
||||
"copyAddress": "Adresi Kopyala",
|
||||
"enterCurrentPasswordForDeleteServer": "Lütfen sunucuyu silmek için şifreyi girin",
|
||||
"deleteServerSuccess": "Sunucu başarıyla silindi",
|
||||
"deleteServerConfirmBtn": "Sunucuyu gerçekten sil",
|
||||
"plainServerDescription": "Cwtch sunucularınızı bir parola ile korumanızı öneririz. Bir parola belirlemezseniz, bu cihaza erişimi olan herkes kriptografik anahtarlar da dahil olmak üzere sunucunun hassas bilgilerine erişebilir.",
|
||||
"encryptedServerDescription": "Bir sunucuyu parola ile şifrelemek, sunucuyu bu aygıtı kullanabilecek diğer kişilerden korur. Doğru şifre girilene kadar şifrelenmiş sunucular görüntülenemez veya erişilemez.",
|
||||
"fileSavedTo": "Şuraya kaydedildi",
|
||||
"fileInterrupted": "Kesildi",
|
||||
"fileCheckingStatus": "İndirme durumunu kontrol ediyor",
|
||||
"verfiyResumeButton": "Doğrula\/devam et",
|
||||
"copyServerKeys": "Anahtarları kopyala",
|
||||
"newMessagesLabel": "Yeni Mesajlar",
|
||||
"importLocalServerLabel": "Yerel bir sunucuyu içeri aktar",
|
||||
"importLocalServerSelectText": "Yerel Sunucu Seç",
|
||||
"importLocalServerButton": "%1'i içeri aktar",
|
||||
"groupsOnThisServerLabel": "Bu sunucuda içinde bulunduğum gruplar",
|
||||
"fieldDescriptionLabel": "Açıklama",
|
||||
"manageKnownServersButton": "Bilinen Sunucuları Yönet",
|
||||
"displayNameTooltip": "Lütfen bir ad girin",
|
||||
"manageKnownServersLong": "Bilinen Sunucuları Yönet",
|
||||
"manageKnownServersShort": "Sunucular",
|
||||
"serverTotalMessagesLabel": "Toplam Mesaj",
|
||||
"serverConnectionsLabel": "Bağlantı",
|
||||
"enableExperimentClickableLinks": "Tıklanabilir Linkleri Etkinleştir",
|
||||
"experimentClickableLinksDescription": "Tıklanabilir bağlantılar deneyi, mesajlarda paylaşılan URL'lere tıklamanıza olanak tanır",
|
||||
"settingImagePreviewsDescription": "Görseller ve Profil Resimleri otomatik olarak indirilir ve görüntülenir. Cwtch'u güvenmediğiniz kişilerle iletişim kurmak için kullanıyorsanız bu deneysel özelliği etkinleştirmemenizi öneririz.",
|
||||
"settingDownloadFolder": "İndirilenler Klasörü",
|
||||
"themeColorLabel": "Renk Teması",
|
||||
"loadingCwtch": "Cwtch yükleniyor...",
|
||||
"storageMigrationModalMessage": "Profiller yeni depolama biçimine taşınıyor. Bu işlem birkaç dakika sürebilir...",
|
||||
"msgFileTooBig": "Dosya boyutu 10 GB'ı geçemez",
|
||||
"msgConfirmSend": "Göndermek istediğinize emin misiniz",
|
||||
"btnSendFile": "Dosya Gönder",
|
||||
"torSettingsUseCustomTorServiceConfigurastionDescription": "Varsayılan tor konfigürasyonunu geçersiz kıl. Uyarı: Bu tehlikeli olabilir. Yalnızca ne yaptığınızı biliyorsanız açın.",
|
||||
"torSettingsEnabledAdvanced": "Gelişmiş Tor Konfigürasyonunu Etkinleştir",
|
||||
"torSettingsUseCustomTorServiceConfiguration": "Özel Tor Hizmeti Konfigürasyonunu (torrc) Kullan",
|
||||
"torSettingsEnabledAdvancedDescription": "Sisteminizde mevcut bir Tor hizmetini kullanın, veya Cwtch Tor Hizmeti'nin parametrelerini değiştirin",
|
||||
"torSettingsCustomSocksPort": "Özel SOCKS Portu",
|
||||
"torSettingsCustomControlPortDescription": "Tor proxy'sine kontrol bağlantıları için özel bir port kullan",
|
||||
"torSettingsCustomSocksPortDescription": "Tor proxy'sine veri bağlantıları için özel bir port kullan",
|
||||
"torSettingsCustomControlPort": "Özel Kontrol Portu",
|
||||
"torSettingsErrorSettingPort": "Port Numarası 1 ile 65535 arasında olmalıdır",
|
||||
"settingImagePreviews": "Görsel Önizlemeleri ve Profil Resimleri",
|
||||
"fileSharingSettingsDownloadFolderTooltip": "İndirilen dosyalara farklı bir varsayılan klasör seçmek için gözat.",
|
||||
"labelACNCircuitInfo": "ACN Ağ Bilgisi",
|
||||
"labelTorNetwork": "Tor Ağı",
|
||||
"torSettingsEnableCache": "Tor Uzlaşmasını Önbelleğe Al",
|
||||
"notificationPolicyDefaultAll": "Tümü Varsayılan",
|
||||
"conversationNotificationPolicyDefault": "Varsayılan",
|
||||
"conversationNotificationPolicyNever": "Asla",
|
||||
"notificationPolicySettingLabel": "Bildirim İlkeleri",
|
||||
"notificationContentSettingLabel": "Bildirim İçeriği",
|
||||
"notificationPolicySettingDescription": "Varsayılan uygulama bildirim davranışını belirler",
|
||||
"notificationContentSettingDescription": "Sohbet bildirimlerinin içeriğini belirler",
|
||||
"tooltipUnpinConversation": "Sohbetin \"Sohbetler\"in üstüne sabitlemesini kaldır",
|
||||
"tooltipPinConversation": "Sohbeti \"Sohbetler\"in üstüne sabitle",
|
||||
"newMessageNotificationConversationInfo": "%1 Sohbetinde Yeni Mesaj",
|
||||
"notificationContentContactInfo": "Sohbet Bilgileri",
|
||||
"conversationNotificationPolicySettingLabel": "Sohbet Bildirim İlkeleri",
|
||||
"conversationNotificationPolicySettingDescription": "Bu sohbet için bildirim ayarlarını kontrol et",
|
||||
"settingGroupBehaviour": "Davranış",
|
||||
"settingsGroupAppearance": "Görünüş",
|
||||
"settingsGroupExperiments": "Deneyler",
|
||||
"newMessageNotificationSimple": "Yeni Mesaj",
|
||||
"exportProfile": "Profili Dışa Aktar",
|
||||
"importProfile": "Profili İçe Aktar",
|
||||
"clickableLinkError": "URL açılmaya çalışılırken hata oluştu",
|
||||
"failedToImportProfile": "Profil İçe Aktarılırken Hata Oluştu",
|
||||
"successfullyImportedProfile": "Profil Başarıyla İçe Aktarıldı: %profile",
|
||||
"shuttingDownApp": "Kapanıyor...",
|
||||
"clickableLinkOpen": "URL'yi aç",
|
||||
"clickableLinksCopy": "URL'yi kopyala",
|
||||
"formattingExperiment": "Mesaj Biçimlendirme",
|
||||
"messageFormattingDescription": "Görüntülenen mesajlarda zengin metin biçimlendirmesini etkinleştirin, örneğin **kalın** ve *italik*",
|
||||
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Bu özellik Gruplar Özelliği'nin Ayarlar'dan etkinleştirilmesini gerektirir",
|
||||
"settingAndroidPowerExemption": "Android Pil Optimizasyonlarını Yoksay",
|
||||
"settingsAndroidPowerReenablePopup": "Cwtch içinden Pil Optimizasyonu yeniden etkinleştirilemiyor. Lütfen Android \/ Ayarlar \/ Uygulamalar \/ Cwtch \/ Pil sayfasına gidin ve 'Pil Kullanımını Yönet' bölümünün altında 'Optimize edilmiş'e basın",
|
||||
"okButton": "OK",
|
||||
"tooltipBoldText": "Kalın",
|
||||
"tooltipBackToMessageEditing": "Mesaj Düzenlemeye Geri Dön",
|
||||
"tooltipItalicize": "İtalik",
|
||||
"tooltipSuperscript": "Üst Simge",
|
||||
"tooltipSubscript": "Alt Simge",
|
||||
"tooltipStrikethrough": "Üstü Çizili",
|
||||
"tooltipPreviewFormatting": "Mesaj Biçimlendirmesini Önizle",
|
||||
"manageSharedFiles": "Paylaşılan Dosyaları Yönet",
|
||||
"stopSharingFile": "Dosya Paylaşımını Durdur",
|
||||
"restartFileShare": "Dosya Paylaşımını Başlat",
|
||||
"viewReplies": "Mesaja gelen yanıtları görüntüle",
|
||||
"headingReplies": "Yanıtlar",
|
||||
"messageNoReplies": "Bu mesaja yanıt gelmemiş.",
|
||||
"fileDownloadUnavailable": "Bu dosya indirmeye uygun görünmüyor. Gönderen dosyanın indirilmesini engellemiş olabilir.",
|
||||
"replyingTo": "%1 hesabına yanıt veriliyor"
|
||||
}
|
|
@ -138,4 +138,209 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''');
|
||||
|
||||
yield LicenseEntryWithLineBreaks(["Roboto fonts"], '''
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
''');
|
||||
}
|
||||
|
|
|
@ -132,6 +132,7 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
|||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
title: 'Cwtch',
|
||||
showSemanticsDebugger: settings.useSemanticDebugger,
|
||||
theme: mkThemeData(settings),
|
||||
home: (!appState.cwtchInit || appState.modalState != ModalState.none) ? SplashView() : ProfileMgrView(),
|
||||
),
|
||||
|
@ -153,9 +154,8 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
|||
Widget continueButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchAction),
|
||||
onPressed: () {
|
||||
// Directly call the shutdown command, Android will do this for us...
|
||||
Provider.of<FlwtchState>(navKey.currentContext!, listen: false).shutdown();
|
||||
Provider.of<AppState>(navKey.currentContext!, listen: false).cwtchIsClosing = true;
|
||||
Navigator.of(navKey.currentContext!).pop(); // dismiss dialog
|
||||
});
|
||||
|
||||
// set up the AlertDialog
|
||||
|
@ -175,10 +175,17 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
|||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
);
|
||||
).then((val) {
|
||||
if (Provider.of<AppState>(navKey.currentContext!, listen: false).cwtchIsClosing) {
|
||||
globalAppState.SetModalState(ModalState.shutdown);
|
||||
// Directly call the shutdown command, Android will do this for us...
|
||||
Provider.of<FlwtchState>(navKey.currentContext!, listen: false).shutdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> shutdown() async {
|
||||
globalAppState.SetModalState(ModalState.shutdown);
|
||||
await cwtch.Shutdown();
|
||||
// Wait a few seconds as shutting down things takes a little time..
|
||||
Future.delayed(Duration(seconds: 1)).then((value) {
|
||||
|
@ -217,9 +224,9 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
|||
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = convoId;
|
||||
|
||||
Navigator.of(navKey.currentContext!).push(
|
||||
MaterialPageRoute<void>(
|
||||
PageRouteBuilder(
|
||||
settings: RouteSettings(name: "conversations"),
|
||||
builder: (BuildContext buildcontext) {
|
||||
pageBuilder: (c, a1, a2) {
|
||||
return OrientationBuilder(builder: (orientationBuilderContext, orientation) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider<ProfileInfoState>.value(value: profile), ChangeNotifierProvider<ContactListState>.value(value: profile.contactList)],
|
||||
|
@ -230,6 +237,8 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
|||
});
|
||||
});
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -248,6 +257,7 @@ class FlwtchState extends State<Flwtch> with WindowListener {
|
|||
|
||||
@override
|
||||
void dispose() async {
|
||||
globalAppState.SetModalState(ModalState.shutdown);
|
||||
await cwtch.Shutdown();
|
||||
windowManager.removeListener(this);
|
||||
cwtch.dispose();
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum ModalState { none, storageMigration }
|
||||
enum ModalState { none, storageMigration, shutdown }
|
||||
|
||||
class AppState extends ChangeNotifier {
|
||||
bool cwtchInit = false;
|
||||
|
@ -42,6 +42,7 @@ class AppState extends ChangeNotifier {
|
|||
|
||||
String? get selectedProfile => _selectedProfile;
|
||||
set selectedProfile(String? newVal) {
|
||||
this._selectedConversation = null;
|
||||
this._selectedProfile = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:cwtch/main.dart';
|
||||
import 'package:cwtch/models/profile.dart';
|
||||
import 'package:cwtch/widgets/messagerow.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'message.dart';
|
||||
import 'messagecache.dart';
|
||||
|
@ -42,16 +46,19 @@ class ContactInfoState extends ChangeNotifier {
|
|||
late int _totalMessages = 0;
|
||||
late DateTime _lastMessageTime;
|
||||
late Map<String, GlobalKey<MessageRowState>> keys;
|
||||
int _newMarker = 0;
|
||||
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;
|
||||
String? _server;
|
||||
late bool _archived;
|
||||
late bool _pinned;
|
||||
|
||||
int _antispamTickets = 0;
|
||||
String? _acnCircuit;
|
||||
String? _messageDraft;
|
||||
|
||||
ContactInfoState(this.profileOnion, this.identifier, this.onion,
|
||||
{nickname = "",
|
||||
|
@ -67,7 +74,8 @@ class ContactInfoState extends ChangeNotifier {
|
|||
lastMessageTime,
|
||||
server,
|
||||
archived = false,
|
||||
notificationPolicy = "ConversationNotificationPolicy.Default"}) {
|
||||
notificationPolicy = "ConversationNotificationPolicy.Default",
|
||||
pinned = false}) {
|
||||
this._nickname = nickname;
|
||||
this._isGroup = isGroup;
|
||||
this._accepted = accepted;
|
||||
|
@ -82,16 +90,26 @@ class ContactInfoState extends ChangeNotifier {
|
|||
this._server = server;
|
||||
this._archived = archived;
|
||||
this._notificationPolicy = notificationPolicyFromString(notificationPolicy);
|
||||
this.messageCache = new MessageCache();
|
||||
this.messageCache = new MessageCache(_totalMessages);
|
||||
this._pinned = pinned;
|
||||
keys = Map<String, GlobalKey<MessageRowState>>();
|
||||
}
|
||||
|
||||
String get nickname => this._nickname;
|
||||
String get nickname => this._nickname + (this._messageDraft != null && this._messageDraft != "" ? "*" : "");
|
||||
|
||||
String get savePeerHistory => this._savePeerHistory;
|
||||
|
||||
String? get acnCircuit => this._acnCircuit;
|
||||
|
||||
String? get messageDraft => this._messageDraft;
|
||||
|
||||
set antispamTickets(int antispamTickets) {
|
||||
this._antispamTickets = antispamTickets;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get antispamTickets => this._antispamTickets;
|
||||
|
||||
set acnCircuit(String? acnCircuit) {
|
||||
this._acnCircuit = acnCircuit;
|
||||
notifyListeners();
|
||||
|
@ -145,44 +163,36 @@ class ContactInfoState extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
set messageDraft(String? newVal) {
|
||||
this._messageDraft = newVal;
|
||||
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) {
|
||||
// don't reset newMarker position when unreadMessages is being cleared
|
||||
if (newVal > 0) {
|
||||
this._newMarker = newVal;
|
||||
} else {
|
||||
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2));
|
||||
}
|
||||
this._unreadMessages = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get newMarker {
|
||||
if (DateTime.now().isAfter(this._newMarkerClearAt)) {
|
||||
// perform heresy
|
||||
this._newMarker = 0;
|
||||
// no need to notifyListeners() because presumably this getter is
|
||||
// being called from a renderer anyway
|
||||
}
|
||||
return this._newMarker;
|
||||
}
|
||||
|
||||
// what's a getter that sometimes sets without a setter
|
||||
// that sometimes doesn't set
|
||||
set newMarker(int newVal) {
|
||||
// only unreadMessages++ can set newMarker = 1;
|
||||
// avoids drawing a marker when the convo is already open
|
||||
if (newVal >= 1) {
|
||||
this._newMarker = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
int get newMarkerMsgIndex {
|
||||
return this._newMarkerMsgIndex;
|
||||
}
|
||||
|
||||
int get totalMessages => this._totalMessages;
|
||||
|
||||
set totalMessages(int newVal) {
|
||||
this._totalMessages = newVal;
|
||||
this.messageCache.storageMessageCount = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -251,11 +261,16 @@ class ContactInfoState extends ChangeNotifier {
|
|||
return ret;
|
||||
}
|
||||
|
||||
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) {
|
||||
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash, bool selectedConversation) {
|
||||
if (!selectedConversation) {
|
||||
unreadMessages++;
|
||||
}
|
||||
if (_newMarkerMsgIndex == -1) {
|
||||
if (!selectedConversation) {
|
||||
_newMarkerMsgIndex = 0;
|
||||
}
|
||||
} else {
|
||||
newMarker++;
|
||||
_newMarkerMsgIndex++;
|
||||
}
|
||||
|
||||
this._lastMessageTime = timestamp;
|
||||
|
@ -292,4 +307,31 @@ class ContactInfoState extends ChangeNotifier {
|
|||
}
|
||||
return ConversationNotificationPolicy.Never;
|
||||
}
|
||||
|
||||
bool get pinned {
|
||||
return _pinned;
|
||||
}
|
||||
|
||||
// Pin the conversation to the top of the conversation list
|
||||
// Requires caller tree to contain a FlwtchState and ProfileInfoState provider.
|
||||
void pin(context) {
|
||||
_pinned = true;
|
||||
var profileHandle = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileHandle, identifier, "profile.pinned", "true");
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Unpin the conversation from the top of the conversation list
|
||||
// Requires caller tree to contain a FlwtchState and ProfileInfoState provider.
|
||||
void unpin(context) {
|
||||
_pinned = false;
|
||||
var profileHandle = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileHandle, identifier, "profile.pinned", "false");
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// returns true only if the conversation has been accepted, and has not been blocked
|
||||
bool isAccepted() {
|
||||
return _accepted && !_blocked;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class ContactListState extends ChangeNotifier {
|
|||
}
|
||||
|
||||
List<ContactInfoState> filteredList() {
|
||||
if (!isFiltered) return contacts;
|
||||
if (!isFiltered) return _contacts;
|
||||
return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList();
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,9 @@ class ContactListState extends ChangeNotifier {
|
|||
// return -1 = a first in list
|
||||
// return 1 = b first in list
|
||||
|
||||
// pinned contacts first
|
||||
if (a.pinned != true && b.pinned == true) return 1;
|
||||
if (a.pinned == true && b.pinned != true) return -1;
|
||||
// blocked contacts last
|
||||
if (a.isBlocked == true && b.isBlocked != true) return 1;
|
||||
if (a.isBlocked != true && b.isBlocked == true) return -1;
|
||||
|
@ -123,8 +126,12 @@ class ContactListState extends ChangeNotifier {
|
|||
return idx >= 0 ? _contacts[idx] : null;
|
||||
}
|
||||
|
||||
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) {
|
||||
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash, bool selectedConversation) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@ class FileDownloadProgress {
|
|||
String? downloadedTo;
|
||||
DateTime? timeStart;
|
||||
DateTime? timeEnd;
|
||||
DateTime? requested;
|
||||
|
||||
FileDownloadProgress(this.chunksTotal, this.timeStart);
|
||||
|
||||
double progress() {
|
||||
return 1.0 * chunksDownloaded / chunksTotal;
|
||||
}
|
||||
|
|
|
@ -32,40 +32,38 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CacheHandler {
|
||||
MessageInfo? lookup(MessageCache cache);
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier);
|
||||
void add(MessageCache cache, MessageInfo messageInfo, String contenthash);
|
||||
Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache);
|
||||
}
|
||||
|
||||
class ByIndex implements CacheHandler {
|
||||
|
@ -73,16 +71,77 @@ class ByIndex implements CacheHandler {
|
|||
|
||||
ByIndex(this.index);
|
||||
|
||||
MessageInfo? lookup(MessageCache cache) {
|
||||
Future<MessageInfo?> lookup(MessageCache cache) async {
|
||||
var msg = cache.getByIndex(index);
|
||||
return msg;
|
||||
}
|
||||
|
||||
Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
|
||||
// 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 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 (start + amount >= cache.storageMessageCount) {
|
||||
amount = cache.storageMessageCount - start;
|
||||
if (amount <= 0) {
|
||||
return Future.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
cache.lockIndexes(start, start + amount);
|
||||
await fetchAndProcess(start, amount, cwtch, profileOnion, conversationIdentifier, cache);
|
||||
|
||||
return cache.getByIndex(index);
|
||||
}
|
||||
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||
return cwtch.GetMessage(profileOnion, conversationIdentifier, 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;
|
||||
}
|
||||
|
||||
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||
cache.add(messageInfo, index, contenthash);
|
||||
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, start + i);
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
EnvironmentConfig.debugLog("Error: Getting indexed messages $start to ${start + amount} failed parsing: " + e.toString() + " " + stacktrace.toString());
|
||||
} finally {
|
||||
if (i != amount) {
|
||||
cache.malformIndexes(start + i, start + amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add(MessageCache cache, MessageInfo messageInfo) {
|
||||
cache.addIndexed(messageInfo, index);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,16 +150,26 @@ class ById implements CacheHandler {
|
|||
|
||||
ById(this.id);
|
||||
|
||||
MessageInfo? lookup(MessageCache cache) {
|
||||
return cache.getById(id);
|
||||
Future<MessageInfo?> lookup(MessageCache cache) {
|
||||
return Future<MessageInfo?>.value(cache.getById(id));
|
||||
}
|
||||
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||
return cwtch.GetMessageByID(profileOnion, conversationIdentifier, id);
|
||||
Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
|
||||
var rawMessageEnvelope = await cwtch.GetMessageByID(profileOnion, conversationIdentifier, id);
|
||||
var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
|
||||
if (messageInfo == null) {
|
||||
return Future.value(null);
|
||||
}
|
||||
cache.addUnindexed(messageInfo);
|
||||
return Future.value(messageInfo);
|
||||
}
|
||||
|
||||
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||
cache.addUnindexed(messageInfo, contenthash);
|
||||
Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
|
||||
var messageInfo = await lookup(cache);
|
||||
if (messageInfo != null) {
|
||||
return Future.value(messageInfo);
|
||||
}
|
||||
return fetch(cwtch, profileOnion, conversationIdentifier, cache);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,113 +178,123 @@ class ByContentHash implements CacheHandler {
|
|||
|
||||
ByContentHash(this.hash);
|
||||
|
||||
MessageInfo? lookup(MessageCache cache) {
|
||||
return cache.getByContentHash(hash);
|
||||
Future<MessageInfo?> lookup(MessageCache cache) {
|
||||
return Future<MessageInfo?>.value(cache.getByContentHash(hash));
|
||||
}
|
||||
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||
return cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
|
||||
}
|
||||
|
||||
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||
cache.addUnindexed(messageInfo, contenthash);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) {
|
||||
var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false);
|
||||
// Hit cache
|
||||
MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, cacheHandler);
|
||||
if (messageInfo != null) {
|
||||
return Future.value(compileOverlay(messageInfo.metadata, messageInfo.wrapper));
|
||||
}
|
||||
|
||||
// Fetch and Cache
|
||||
var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler);
|
||||
return messageInfoFuture.then((MessageInfo? messageInfo) {
|
||||
if (messageInfo != null) {
|
||||
return compileOverlay(messageInfo.metadata, messageInfo.wrapper);
|
||||
} else {
|
||||
return MalformedMessage(malformedMetadata);
|
||||
Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
|
||||
var rawMessageEnvelope = await cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
|
||||
var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
|
||||
if (messageInfo == null) {
|
||||
return Future.value(null);
|
||||
}
|
||||
});
|
||||
cache.addUnindexed(messageInfo);
|
||||
return Future.value(messageInfo);
|
||||
}
|
||||
|
||||
Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
|
||||
var messageInfo = await lookup(cache);
|
||||
if (messageInfo != null) {
|
||||
return Future.value(messageInfo);
|
||||
}
|
||||
return fetch(cwtch, profileOnion, conversationIdentifier, cache);
|
||||
}
|
||||
}
|
||||
|
||||
MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) {
|
||||
// Hit cache
|
||||
List<Message> getReplies(MessageCache cache, int messageIdentifier) {
|
||||
List<Message> replies = List.empty(growable: true);
|
||||
|
||||
try {
|
||||
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
|
||||
if (cache != null) {
|
||||
MessageInfo? messageInfo = cacheHandler.lookup(cache);
|
||||
if (messageInfo != null) {
|
||||
return messageInfo;
|
||||
MessageInfo original = cache.cache[messageIdentifier]!;
|
||||
String hash = original.metadata.contenthash;
|
||||
|
||||
cache.cache.forEach((key, messageInfo) {
|
||||
// only bother searching for identifiers that came *after*
|
||||
if (key > messageIdentifier) {
|
||||
try {
|
||||
dynamic message = jsonDecode(messageInfo.wrapper);
|
||||
var content = message['d'] as dynamic;
|
||||
dynamic qmessage = jsonDecode(content);
|
||||
if (qmessage["body"] == null || qmessage["quotedHash"] == null) {
|
||||
return;
|
||||
}
|
||||
if (qmessage["quotedHash"] == hash) {
|
||||
replies.add(compileOverlay(messageInfo));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
|
||||
}
|
||||
|
||||
replies.sort((a, b) {
|
||||
return a.getMetadata().messageID.compareTo(b.getMetadata().messageID);
|
||||
});
|
||||
|
||||
return replies;
|
||||
}
|
||||
|
||||
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) async {
|
||||
var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false, "");
|
||||
var cwtch = Provider.of<FlwtchState>(context, listen: false).cwtch;
|
||||
|
||||
MessageCache? cache;
|
||||
try {
|
||||
cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
|
||||
if (cache == null) {
|
||||
EnvironmentConfig.debugLog("error: cannot get message cache for profile: $profileOnion conversation: $conversationIdentifier");
|
||||
return MalformedMessage(malformedMetadata);
|
||||
}
|
||||
} catch (e) {
|
||||
EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
|
||||
// provider check failed...make an expensive call...
|
||||
return MalformedMessage(malformedMetadata);
|
||||
}
|
||||
|
||||
MessageInfo? messageInfo = await cacheHandler.get(cwtch, profileOnion, conversationIdentifier, cache);
|
||||
|
||||
if (messageInfo != null) {
|
||||
return compileOverlay(messageInfo);
|
||||
} else {
|
||||
return MalformedMessage(malformedMetadata);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<MessageInfo?> fetchAndCacheMessageInfo(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) {
|
||||
// Load and cache
|
||||
var profileInfostate = Provider.of<ProfileInfoState>(context, listen: false);
|
||||
MessageInfo? messageJsonToInfo(String profileOnion, int conversationIdentifier, dynamic messageJson) {
|
||||
try {
|
||||
Future<dynamic> rawMessageEnvelopeFuture;
|
||||
dynamic messageWrapper = jsonDecode(messageJson);
|
||||
|
||||
rawMessageEnvelopeFuture = cacheHandler.fetch(Provider.of<FlwtchState>(context, listen: false).cwtch, profileOnion, conversationIdentifier);
|
||||
if (messageWrapper == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
|
||||
try {
|
||||
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
|
||||
// There are 2 conditions in which this error condition can be met:
|
||||
// 1. The application == nil, in which case this instance of the UI is already
|
||||
// broken beyond repair, and will either be replaced by a new version, or requires a complete
|
||||
// restart.
|
||||
// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion.
|
||||
// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the
|
||||
// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one
|
||||
// will find itself delayed.
|
||||
// The second case is recoverable by tail-recursing this future.
|
||||
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
|
||||
return Future.delayed(Duration(seconds: 2), () {
|
||||
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
|
||||
return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler);
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the initial metadata
|
||||
var messageID = messageWrapper['ID'];
|
||||
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
|
||||
var senderHandle = messageWrapper['PeerID'];
|
||||
var senderImage = messageWrapper['ContactImage'];
|
||||
var attributes = messageWrapper['Attributes'];
|
||||
var ackd = messageWrapper['Acknowledged'];
|
||||
var error = messageWrapper['Error'] != null;
|
||||
var signature = messageWrapper['Signature'];
|
||||
var contenthash = messageWrapper['ContentHash'];
|
||||
var localIndex = messageWrapper['LocalIndex'];
|
||||
var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false);
|
||||
var messageInfo = new MessageInfo(metadata, messageWrapper['Message']);
|
||||
|
||||
var cache = profileInfostate.contactList.getContact(conversationIdentifier)?.messageCache;
|
||||
if (cache != null) {
|
||||
cacheHandler.add(cache, messageInfo, contenthash);
|
||||
}
|
||||
|
||||
return messageInfo;
|
||||
} catch (e, stacktrace) {
|
||||
EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString() + " " + stacktrace.toString());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
EnvironmentConfig.debugLog("message handler exeption on get message: $e");
|
||||
return Future.value(null);
|
||||
return messageWrapperToInfo(profileOnion, conversationIdentifier, messageWrapper);
|
||||
} catch (e, stacktrace) {
|
||||
EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString() + " " + stacktrace.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MessageInfo messageWrapperToInfo(String profileOnion, int conversationIdentifier, dynamic messageWrapper) {
|
||||
// Construct the initial metadata
|
||||
var messageID = messageWrapper['ID'];
|
||||
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
|
||||
var senderHandle = messageWrapper['PeerID'];
|
||||
var senderImage = messageWrapper['ContactImage'];
|
||||
var attributes = messageWrapper['Attributes'];
|
||||
var ackd = messageWrapper['Acknowledged'];
|
||||
var error = messageWrapper['Error'] != null;
|
||||
var signature = messageWrapper['Signature'];
|
||||
var contenthash = messageWrapper['ContentHash'];
|
||||
var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false, contenthash);
|
||||
var messageInfo = new MessageInfo(metadata, messageWrapper['Message']);
|
||||
|
||||
return messageInfo;
|
||||
}
|
||||
|
||||
class MessageMetadata extends ChangeNotifier {
|
||||
// meta-metadata
|
||||
final String profileOnion;
|
||||
|
@ -231,6 +310,7 @@ class MessageMetadata extends ChangeNotifier {
|
|||
final bool isAuto;
|
||||
|
||||
final String? signature;
|
||||
final String contenthash;
|
||||
|
||||
dynamic get attributes => this._attributes;
|
||||
|
||||
|
@ -248,6 +328,6 @@ class MessageMetadata extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
MessageMetadata(
|
||||
this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error, this.isAuto);
|
||||
MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error,
|
||||
this.isAuto, this.contenthash);
|
||||
}
|
||||
|
|
|
@ -1,60 +1,169 @@
|
|||
import 'dart:async';
|
||||
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 {
|
||||
final MessageMetadata metadata;
|
||||
final String wrapper;
|
||||
late MessageMetadata metadata;
|
||||
late String wrapper;
|
||||
|
||||
MessageInfo(this.metadata, this.wrapper);
|
||||
|
||||
int size() {
|
||||
var wrapperSize = wrapper.length * 2;
|
||||
return wrapperSize;
|
||||
}
|
||||
}
|
||||
|
||||
class LocalIndexMessage {
|
||||
late bool cacheOnly;
|
||||
late bool isLoading;
|
||||
late Future<void> loaded;
|
||||
late Completer<void> loader;
|
||||
|
||||
late int? messageId;
|
||||
|
||||
LocalIndexMessage(int? messageId, {cacheOnly = false, isLoading = false}) {
|
||||
this.messageId = messageId;
|
||||
this.cacheOnly = cacheOnly;
|
||||
this.isLoading = isLoading;
|
||||
if (isLoading) {
|
||||
loader = Completer<void>();
|
||||
loaded = loader.future;
|
||||
}
|
||||
}
|
||||
|
||||
void finishLoad(int messageId) {
|
||||
this.messageId = messageId;
|
||||
isLoading = false;
|
||||
loader.complete(true);
|
||||
}
|
||||
|
||||
void failLoad() {
|
||||
this.messageId = null;
|
||||
isLoading = false;
|
||||
loader.complete(true);
|
||||
}
|
||||
|
||||
Future<void> waitForLoad() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
Future<int?> get() async {
|
||||
if (isLoading) {
|
||||
await waitForLoad();
|
||||
}
|
||||
return messageId;
|
||||
}
|
||||
}
|
||||
|
||||
// Message cache stores messages for use by the UI and uses MessageHandler and associated ByX loaders
|
||||
// the cache stores messages in a cache indexed by their storage Id, and has two secondary indexes into it, content hash, and local index
|
||||
// Index is the primary way to access the cache as it is a sequential ordered access and is used by the message pane
|
||||
// contentHash is used for fetching replies
|
||||
// by Id is used when composing a reply
|
||||
// cacheByIndex supports additional features than just a direct index into the cache (byID)
|
||||
// it allows locking of ranges in order to support bulk sequential loading (see ByIndex in message.dart)
|
||||
// cacheByIndex allows allows inserting temporarily non storage backed messages so that Send Message can be respected instantly and then updated upon insertion into backend
|
||||
// the message cache needs storageMessageCount maintained by the system so it can inform bulk loading when it's reaching the end of fetchable messages
|
||||
class MessageCache extends ChangeNotifier {
|
||||
// cache of MessageId to Message
|
||||
late Map<int, MessageInfo> cache;
|
||||
late List<int?> cacheByIndex;
|
||||
|
||||
// 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;
|
||||
|
||||
MessageCache() {
|
||||
late int _storageMessageCount;
|
||||
|
||||
MessageCache(int storageMessageCount) {
|
||||
cache = {};
|
||||
cacheByIndex = List.empty(growable: true);
|
||||
cacheByHash = {};
|
||||
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];
|
||||
MessageInfo? getByIndex(int index) {
|
||||
|
||||
Future<MessageInfo?> getByIndex(int index) async {
|
||||
if (index >= cacheByIndex.length) {
|
||||
return null;
|
||||
}
|
||||
return cache[cacheByIndex[index]];
|
||||
var id = await cacheByIndex[index].get();
|
||||
if (id == null) {
|
||||
return Future<MessageInfo?>.value(null);
|
||||
}
|
||||
return cache[id];
|
||||
}
|
||||
|
||||
int findIndex(int id) {
|
||||
return cacheByIndex.indexWhere((element) => element.messageId == id);
|
||||
}
|
||||
|
||||
MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]];
|
||||
|
||||
void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash) {
|
||||
this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data);
|
||||
this.cacheByIndex.insert(0, messageID);
|
||||
void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash) {
|
||||
this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto, contenthash), data);
|
||||
this.cacheByIndex.insert(0, LocalIndexMessage(messageID));
|
||||
if (contenthash != null && contenthash != "") {
|
||||
this.cacheByHash[contenthash] = messageID;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void add(MessageInfo messageInfo, int index, String? contenthash) {
|
||||
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||
this.cacheByIndex.insert(index, messageInfo.metadata.messageID);
|
||||
if (contenthash != null && contenthash != "") {
|
||||
this.cacheByHash[contenthash] = messageInfo.metadata.messageID;
|
||||
// inserts place holder values into the index cache that will block on .get() until .finishLoad() is called on them with message contents
|
||||
// or .failLoad() is called on them to mark them malformed
|
||||
// this prevents successive ui message build requests from triggering multiple GetMesssage requests to the backend, as the first one locks a block of messages and the rest wait on that
|
||||
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--;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addUnindexed(MessageInfo messageInfo, String? contenthash) {
|
||||
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||
if (contenthash != null && contenthash != "") {
|
||||
this.cacheByHash[contenthash] = messageInfo.metadata.messageID;
|
||||
void malformIndexes(int start, int end) {
|
||||
for (var i = start; i < end; i++) {
|
||||
this.cacheByIndex[i].failLoad();
|
||||
}
|
||||
}
|
||||
|
||||
void addIndexed(MessageInfo messageInfo, int index) {
|
||||
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||
if (index < this.cacheByIndex.length) {
|
||||
this.cacheByIndex[index].finishLoad(messageInfo.metadata.messageID);
|
||||
} else {
|
||||
this.cacheByIndex.insert(index, LocalIndexMessage(messageInfo.metadata.messageID));
|
||||
}
|
||||
this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
|
||||
}
|
||||
|
||||
void addUnindexed(MessageInfo messageInfo) {
|
||||
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||
if (messageInfo.metadata.contenthash != "") {
|
||||
this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void ackCache(int messageID) {
|
||||
|
@ -66,4 +175,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,6 @@ 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 +30,13 @@ 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,
|
||||
);
|
||||
} catch (e) {
|
||||
return MalformedBubble();
|
||||
}
|
||||
|
@ -48,21 +49,22 @@ 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);
|
||||
|
||||
if (message["body"] == null || message["quotedHash"] == null) {
|
||||
return MalformedBubble();
|
||||
return MalformedMessage(this.metadata).getWidget(context, key, index);
|
||||
}
|
||||
|
||||
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();
|
||||
return MalformedMessage(this.metadata).getWidget(context, key, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2,10 +2,12 @@ import 'dart:convert';
|
|||
|
||||
import 'package:cwtch/models/remoteserver.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'contact.dart';
|
||||
import 'contactlist.dart';
|
||||
import 'filedownloadprogress.dart';
|
||||
import 'messagecache.dart';
|
||||
import 'profileservers.dart';
|
||||
|
||||
class ProfileInfoState extends ChangeNotifier {
|
||||
|
@ -19,7 +21,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
bool _online = false;
|
||||
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
|
||||
Map<String, int> _downloadTriggers = Map<String, int>();
|
||||
|
||||
ItemScrollController contactListScrollController = new ItemScrollController();
|
||||
// assume profiles are encrypted...this will be set to false
|
||||
// in the constructor if the profile is encrypted with the defacto password.
|
||||
bool _encrypted = true;
|
||||
|
@ -50,6 +52,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
|
||||
List<dynamic> contacts = jsonDecode(contactsJson);
|
||||
this._contacts.addAll(contacts.map((contact) {
|
||||
this._unreadMessages += contact["numUnread"] as int;
|
||||
return ContactInfoState(this.onion, contact["identifier"], contact["onion"],
|
||||
nickname: contact["name"],
|
||||
status: contact["status"],
|
||||
|
@ -64,6 +67,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
server: contact["groupServer"],
|
||||
archived: contact["isArchived"] == true,
|
||||
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
|
||||
pinned: contact["attributes"]?["local.profile.pinned"] == "true",
|
||||
notificationPolicy: contact["notificationPolicy"] ?? "ConversationNotificationPolicy.Default");
|
||||
}));
|
||||
|
||||
|
@ -164,15 +168,22 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
this._nickname = name;
|
||||
this._imagePath = picture;
|
||||
this._online = online;
|
||||
this._unreadMessages = 0;
|
||||
this.replaceServers(serverJson);
|
||||
|
||||
if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
|
||||
List<dynamic> contacts = jsonDecode(contactsJson);
|
||||
contacts.forEach((contact) {
|
||||
var profileContact = this._contacts.getContact(contact["identifier"]);
|
||||
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 {
|
||||
|
@ -195,14 +206,13 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
notificationPolicy: contact["notificationPolicy"] ?? "ConversationNotificationPolicy.Default",
|
||||
));
|
||||
}
|
||||
unreadMessages += int.parse(contact["numUnread"]);
|
||||
});
|
||||
}
|
||||
this._contacts.resort();
|
||||
}
|
||||
|
||||
void newMessage(
|
||||
int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedProfile, bool selectedConversation) {
|
||||
int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash, bool selectedProfile, bool selectedConversation) {
|
||||
if (!selectedProfile) {
|
||||
unreadMessages++;
|
||||
notifyListeners();
|
||||
|
@ -282,12 +292,27 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
}
|
||||
|
||||
bool downloadInterrupted(String fileKey) {
|
||||
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted;
|
||||
if (this._downloads.containsKey(fileKey)) {
|
||||
if (this._downloads[fileKey]!.interrupted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._downloads[fileKey]!.requested != null) {
|
||||
if (DateTime.now().difference(this._downloads[fileKey]!.requested!) > Duration(minutes: 1)) {
|
||||
this._downloads[fileKey]!.requested = null;
|
||||
this._downloads[fileKey]!.interrupted = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void downloadMarkResumed(String fileKey) {
|
||||
if (this._downloads.containsKey(fileKey)) {
|
||||
this._downloads[fileKey]!.interrupted = false;
|
||||
this._downloads[fileKey]!.requested = DateTime.now();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -337,4 +362,13 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
_downloadTriggers[fileKey] = identifier;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int cacheMemUsage() {
|
||||
return _contacts.cacheMemUsage();
|
||||
}
|
||||
|
||||
void downloadReset(String fileKey) {
|
||||
this._downloads.remove(fileKey);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:cwtch/config.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'contact.dart';
|
||||
|
@ -13,7 +14,7 @@ class RemoteServerInfoState extends ChangeNotifier {
|
|||
DateTime lastPreSyncMessagTime = new DateTime(2020);
|
||||
|
||||
RemoteServerInfoState(this.onion, this.identifier, this.description, this._status, {lastPreSyncMessageTime, mostRecentMessageTime}) {
|
||||
if (_status == "Authenticated") {
|
||||
if (_status == "Authenticated" || _status == "Synced") {
|
||||
this.lastPreSyncMessagTime = lastPreSyncMessageTime;
|
||||
updateSyncProgressFor(mostRecentMessageTime);
|
||||
}
|
||||
|
@ -52,6 +53,13 @@ class RemoteServerInfoState extends ChangeNotifier {
|
|||
void updateSyncProgressFor(DateTime point) {
|
||||
var range = lastPreSyncMessagTime.toUtc().difference(DateTime.now().toUtc());
|
||||
var pointFromStart = lastPreSyncMessagTime.toUtc().difference(point.toUtc());
|
||||
if (!pointFromStart.isNegative) {
|
||||
// ! is Negative cus all the duration's we're calculating incidently are negative
|
||||
// this message is from before we think we should be syncing with the server
|
||||
// Can be because of a new server or a full resync, either way, use this (oldest message) as our lastPreSyncMessageTime
|
||||
this.lastPreSyncMessagTime = point;
|
||||
pointFromStart = lastPreSyncMessagTime.toUtc().difference(point.toUtc());
|
||||
}
|
||||
syncProgress = pointFromStart.inSeconds / range.inSeconds;
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
|
@ -3,11 +3,14 @@ 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';
|
||||
import 'package:flutter_local_notifications_linux/src/model/icon.dart';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
|
@ -72,28 +75,56 @@ class NotificationPayload {
|
|||
};
|
||||
}
|
||||
|
||||
// FlutterLocalNotificationsPlugin based NotificationManager that handles Linux and MacOS
|
||||
// Todo: it can also handle Android, do we want to migrate away from our manual solution?
|
||||
// FlutterLocalNotificationsPlugin based NotificationManager that handles MacOS and Linux
|
||||
// TODO: Windows support is being worked on, check back and migrate to that too when it lands
|
||||
class NixNotificationManager implements NotificationsManager {
|
||||
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||
late Future<void> Function(String, int) notificationSelectConvo;
|
||||
late String linuxAssetsPath;
|
||||
|
||||
// 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 "";
|
||||
}
|
||||
|
||||
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 {
|
||||
if (Platform.isLinux) {
|
||||
linuxAssetsPath = await detectLinuxAssetsPath();
|
||||
} else {
|
||||
linuxAssetsPath = "";
|
||||
}
|
||||
|
||||
final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(defaultPresentSound: false);
|
||||
var linuxIcon = FilePathLinuxIcon(path.join(linuxAssetsPath, 'assets/knott.png'));
|
||||
|
||||
final LinuxInitializationSettings initializationSettingsLinux = LinuxInitializationSettings(defaultActionName: 'Open notification', defaultIcon: linuxIcon, defaultSuppressSound: true);
|
||||
|
||||
final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux);
|
||||
|
||||
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
|
||||
alert: true,
|
||||
badge: false,
|
||||
sound: false,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification);
|
||||
});
|
||||
}
|
||||
|
@ -101,7 +132,12 @@ class NixNotificationManager implements NotificationsManager {
|
|||
Future<void> notify(String message, String profile, int conversationId) async {
|
||||
if (!globalAppState.focus) {
|
||||
// Warning: Only use title field on Linux, body field will render links as clickable
|
||||
await flutterLocalNotificationsPlugin.show(0, message, '', NotificationDetails(linux: LinuxNotificationDetails(suppressSound: true, category: LinuxNotificationCategory.imReceived())),
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
message,
|
||||
'',
|
||||
NotificationDetails(
|
||||
linux: LinuxNotificationDetails(suppressSound: true, category: LinuxNotificationCategory.imReceived(), icon: FilePathLinuxIcon(path.join(linuxAssetsPath, 'assets/knott.png')))),
|
||||
payload: jsonEncode(NotificationPayload(profile, conversationId)));
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +153,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 NixNotificationManager(notificationSelectConvo);
|
||||
} catch (e) {
|
||||
EnvironmentConfig.debugLog("Failed to create LinuxNotificationManager. Switching off notifications.");
|
||||
}
|
||||
} else if (Platform.isMacOS) {
|
||||
try {
|
||||
return NixNotificationManager(notificationSelectConvo);
|
||||
} catch (e) {
|
||||
|
|
|
@ -14,6 +14,8 @@ const ServerManagementExperiment = "servers-experiment";
|
|||
const FileSharingExperiment = "filesharing";
|
||||
const ImagePreviewsExperiment = "filesharing-images";
|
||||
const ClickableLinksExperiment = "clickable-links";
|
||||
const FormattingExperiment = "message-formatting";
|
||||
const QRCodeExperiment = "qrcode-support";
|
||||
|
||||
enum DualpaneMode {
|
||||
Single,
|
||||
|
@ -62,9 +64,17 @@ class Settings extends ChangeNotifier {
|
|||
String _customTorAuth = "";
|
||||
bool _useTorCache = false;
|
||||
String _torCacheDir = "";
|
||||
bool _useSemanticDebugger = false;
|
||||
|
||||
String get torCacheDir => _torCacheDir;
|
||||
|
||||
set useSemanticDebugger(bool newval) {
|
||||
this._useSemanticDebugger = newval;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get useSemanticDebugger => _useSemanticDebugger;
|
||||
|
||||
void setTheme(String themeId, String mode) {
|
||||
theme = getTheme(themeId, mode);
|
||||
notifyListeners();
|
||||
|
@ -84,6 +94,13 @@ class Settings extends ChangeNotifier {
|
|||
return this.experiments[experiment]! == true;
|
||||
}
|
||||
}
|
||||
|
||||
// If message formatting has not explicitly been turned off, then
|
||||
// turn it on by default.
|
||||
if (experiment == FormattingExperiment) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -108,8 +125,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
|
||||
|
@ -274,7 +291,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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
@ -173,7 +174,6 @@ ThemeData mkThemeData(Settings opaque) {
|
|||
? opaque.current().defaultButtonDisabledColor
|
||||
: null),
|
||||
enableFeedback: true,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
|
||||
shape: MaterialStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
|
@ -181,7 +181,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 +210,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),
|
||||
);
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
@ -169,6 +171,9 @@ class SelectableLinkify extends StatelessWidget {
|
|||
|
||||
// TextSpan
|
||||
|
||||
/// Style for code text
|
||||
final TextStyle? codeStyle;
|
||||
|
||||
/// Style for non-link text
|
||||
final TextStyle? style;
|
||||
|
||||
|
@ -253,6 +258,7 @@ class SelectableLinkify extends StatelessWidget {
|
|||
this.linkStyle,
|
||||
// RichText
|
||||
this.textAlign,
|
||||
this.codeStyle,
|
||||
this.textDirection,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
|
@ -289,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
|
||||
|
@ -345,6 +352,7 @@ TextSpan buildTextSpan(
|
|||
List<LinkifyElement> elements, {
|
||||
TextStyle? style,
|
||||
TextStyle? linkStyle,
|
||||
TextStyle? codeStyle,
|
||||
LinkCallback? onOpen,
|
||||
bool useMouseRegion = false,
|
||||
}) {
|
||||
|
@ -357,11 +365,7 @@ TextSpan buildTextSpan(
|
|||
message: element.url,
|
||||
inlineSpan: LinkableSpan(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
inlineSpan: TextSpan(
|
||||
text: element.text,
|
||||
style: linkStyle,
|
||||
recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
|
||||
),
|
||||
inlineSpan: TextSpan(text: element.text, style: linkStyle, recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, semanticsLabel: element.text),
|
||||
));
|
||||
} else {
|
||||
return TooltipSpan(
|
||||
|
@ -372,6 +376,42 @@ TextSpan buildTextSpan(
|
|||
recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
|
||||
));
|
||||
}
|
||||
} else if (element is BoldElement) {
|
||||
return TextSpan(text: element.text.replaceAll("*", ""), style: style?.copyWith(fontWeight: FontWeight.bold), semanticsLabel: element.text);
|
||||
} else if (element is ItalicElement) {
|
||||
return TextSpan(text: element.text.replaceAll("*", ""), style: style?.copyWith(fontStyle: FontStyle.italic), semanticsLabel: element.text);
|
||||
} else if (element is SuperElement) {
|
||||
return WidgetSpan(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(2, -6),
|
||||
child: Text(element.text.replaceAll("^", ""),
|
||||
//superscript is usually smaller in size
|
||||
textScaleFactor: 0.7,
|
||||
style: style,
|
||||
semanticsLabel: element.text),
|
||||
));
|
||||
} else if (element is SubElement) {
|
||||
return WidgetSpan(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(2, 4),
|
||||
child: Text(element.text.replaceAll("_", ""),
|
||||
//superscript is usually smaller in size
|
||||
textScaleFactor: 0.7,
|
||||
style: style,
|
||||
semanticsLabel: element.text),
|
||||
));
|
||||
} else if (element is StrikeElement) {
|
||||
return TextSpan(
|
||||
text: element.text.replaceAll("~~", ""),
|
||||
style: style?.copyWith(decoration: TextDecoration.lineThrough, decorationColor: style.color, decorationStyle: TextDecorationStyle.solid),
|
||||
semanticsLabel: element.text);
|
||||
} else if (element is CodeElement) {
|
||||
return TextSpan(
|
||||
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: codeStyle?.copyWith(fontFamily: "RobotoMono", fontSize: codeStyle.fontSize! - 1.5),
|
||||
semanticsLabel: element.text);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: element.text,
|
||||
|
|
|
@ -37,6 +37,30 @@ abstract class LinkifyElement {
|
|||
bool equals(other) => other is LinkifyElement && other.text == text;
|
||||
}
|
||||
|
||||
class BoldElement extends LinkifyElement {
|
||||
BoldElement(String text) : super(text);
|
||||
}
|
||||
|
||||
class ItalicElement extends LinkifyElement {
|
||||
ItalicElement(String text) : super(text);
|
||||
}
|
||||
|
||||
class SuperElement extends LinkifyElement {
|
||||
SuperElement(String text) : super(text);
|
||||
}
|
||||
|
||||
class SubElement extends LinkifyElement {
|
||||
SubElement(String text) : super(text);
|
||||
}
|
||||
|
||||
class StrikeElement extends LinkifyElement {
|
||||
StrikeElement(String text) : super(text);
|
||||
}
|
||||
|
||||
class CodeElement extends LinkifyElement {
|
||||
CodeElement(String text) : super(text);
|
||||
}
|
||||
|
||||
class LinkableElement extends LinkifyElement {
|
||||
final String url;
|
||||
|
||||
|
@ -81,11 +105,10 @@ class LinkifyOptions {
|
|||
/// Excludes `.` at end of URLs.
|
||||
final bool excludeLastPeriod;
|
||||
|
||||
const LinkifyOptions({
|
||||
this.looseUrl = false,
|
||||
this.defaultToHttps = false,
|
||||
this.excludeLastPeriod = true,
|
||||
});
|
||||
final bool messageFormatting;
|
||||
final bool parseLinks;
|
||||
|
||||
const LinkifyOptions({this.looseUrl = false, this.defaultToHttps = false, this.excludeLastPeriod = true, this.messageFormatting = false, this.parseLinks = false});
|
||||
}
|
||||
|
||||
const _urlLinkifier = UrlLinkifier();
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import 'package:cwtch/config.dart';
|
||||
|
||||
import 'linkify.dart';
|
||||
|
||||
final _urlRegex = RegExp(
|
||||
|
@ -44,56 +46,175 @@ final _protocolIdentifierRegex = RegExp(
|
|||
caseSensitive: false,
|
||||
);
|
||||
|
||||
class Formatter {
|
||||
final RegExp expression;
|
||||
final LinkifyElement Function(String) element;
|
||||
|
||||
Formatter(this.expression, this.element);
|
||||
}
|
||||
|
||||
// regex to match **bold**
|
||||
final _boldRegex = RegExp(
|
||||
r'^(.*?)(\*\*([^*]*)\*\*)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
// regex to match *italic*
|
||||
final _italicRegex = RegExp(
|
||||
r'^(.*?)(\*([^*]*)\*)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
// regex to match ^superscript^
|
||||
final _superRegex = RegExp(
|
||||
r'^(.*?)(\^([^\^]*)\^)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
// regex to match ^subscript^
|
||||
final _subRegex = RegExp(
|
||||
r'^(.*?)(\_([^\_]*)\_)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
// regex to match ~~strikethrough~~
|
||||
final _strikeRegex = RegExp(
|
||||
r'^(.*?)(\~\~([^\~]*)\~\~)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
// regex to match `code`
|
||||
final _codeRegex = RegExp(
|
||||
r'^(.*?)(\`([^\`]*)\`)',
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
class UrlLinkifier extends Linkifier {
|
||||
const UrlLinkifier();
|
||||
|
||||
List<LinkifyElement> replaceAndParse(tle, TextElement element, RegExpMatch match, List<LinkifyElement> list, options) {
|
||||
final text = element.text.replaceFirst(match.group(0)!, '');
|
||||
|
||||
if (match.group(1)?.isNotEmpty == true) {
|
||||
list.addAll(parse([TextElement(match.group(1)!)], options));
|
||||
}
|
||||
|
||||
if (match.group(2)?.isNotEmpty == true) {
|
||||
list.add(tle(match.group(2)!));
|
||||
}
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
list.addAll(parse([TextElement(text)], options));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
List<LinkifyElement> parseFormatting(element, options) {
|
||||
var list = <LinkifyElement>[];
|
||||
|
||||
// code -> bold -> italic -> super -> sub -> strike
|
||||
// not we don't currently allow combinations of these elements the first
|
||||
// one to match a given set will be the only style applied - this will be fixed
|
||||
final formattingPrecedence = [
|
||||
Formatter(_codeRegex, CodeElement.new),
|
||||
Formatter(_boldRegex, BoldElement.new),
|
||||
Formatter(_italicRegex, ItalicElement.new),
|
||||
Formatter(_superRegex, SuperElement.new),
|
||||
Formatter(_subRegex, SubElement.new),
|
||||
Formatter(_strikeRegex, StrikeElement.new)
|
||||
];
|
||||
|
||||
// Loop through the formatters in with precedence and break when something is found...
|
||||
for (var formatter in formattingPrecedence) {
|
||||
var formattingMatch = formatter.expression.firstMatch(element.text);
|
||||
if (formattingMatch != null) {
|
||||
list = replaceAndParse(formatter.element, element, formattingMatch, list, options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// catch all case where we didn't match anything and so need to return back
|
||||
// the unformatted text
|
||||
// conceptually this is Formatter((.*), TextElement.new)
|
||||
if (list.isEmpty) {
|
||||
list.add(element);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@override
|
||||
List<LinkifyElement> parse(elements, options) {
|
||||
final list = <LinkifyElement>[];
|
||||
var list = <LinkifyElement>[];
|
||||
|
||||
elements.forEach((element) {
|
||||
if (element is TextElement) {
|
||||
var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text);
|
||||
|
||||
if (match == null) {
|
||||
if (options.parseLinks == false && options.messageFormatting == false) {
|
||||
list.add(element);
|
||||
} else if (options.parseLinks == true) {
|
||||
// check if there is a link...
|
||||
var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text);
|
||||
|
||||
// if not then we only have to consider formatting...
|
||||
if (match == null) {
|
||||
// only do formatting if message formatting is enabled
|
||||
if (options.messageFormatting == false) {
|
||||
list.add(element);
|
||||
} else {
|
||||
// add all the formatting elements contained in this text
|
||||
list.addAll(parseFormatting(element, options));
|
||||
}
|
||||
} else {
|
||||
final text = element.text.replaceFirst(match.group(0)!, '');
|
||||
|
||||
if (match.group(1)?.isNotEmpty == true) {
|
||||
// we match links first and the feed everything before the link
|
||||
// back through the parser
|
||||
list.addAll(parse([TextElement(match.group(1)!)], options));
|
||||
}
|
||||
|
||||
if (match.group(2)?.isNotEmpty == true) {
|
||||
var originalUrl = match.group(2)!;
|
||||
String? end;
|
||||
|
||||
if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") {
|
||||
end = ".";
|
||||
originalUrl = originalUrl.substring(0, originalUrl.length - 1);
|
||||
}
|
||||
|
||||
var url = originalUrl;
|
||||
|
||||
// If protocol has not been specified then append a protocol
|
||||
// to the start of the URL so that it can be opened...
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
list.add(UrlElement(url, originalUrl));
|
||||
|
||||
if (end != null) {
|
||||
list.add(TextElement(end));
|
||||
}
|
||||
}
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
list.addAll(parse([TextElement(text)], options));
|
||||
}
|
||||
}
|
||||
} else if (options.messageFormatting == true) {
|
||||
// we can jump straight to message formatting...
|
||||
list.addAll(parseFormatting(element, options));
|
||||
} else {
|
||||
final text = element.text.replaceFirst(match.group(0)!, '');
|
||||
|
||||
if (match.group(1)?.isNotEmpty == true) {
|
||||
list.add(TextElement(match.group(1)!));
|
||||
}
|
||||
|
||||
if (match.group(2)?.isNotEmpty == true) {
|
||||
var originalUrl = match.group(2)!;
|
||||
String? end;
|
||||
|
||||
if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") {
|
||||
end = ".";
|
||||
originalUrl = originalUrl.substring(0, originalUrl.length - 1);
|
||||
}
|
||||
|
||||
var url = originalUrl;
|
||||
|
||||
// If protocol has not been specified then append a protocol
|
||||
// to the start of the URL so that it can be opened...
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
list.add(UrlElement(url, originalUrl));
|
||||
|
||||
if (end != null) {
|
||||
list.add(TextElement(end));
|
||||
}
|
||||
}
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
list.addAll(parse([TextElement(text)], options));
|
||||
}
|
||||
// unreachable - if we get here then there is something wrong in the above logic since every combination of
|
||||
// formatting options should have already been accounted for.
|
||||
EnvironmentConfig.debugLog("'unreachable' code path in formatting has been triggered. this is very likely a bug - please report $options");
|
||||
}
|
||||
} else {
|
||||
list.add(element);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
@ -32,6 +36,9 @@ class _AddContactViewState extends State<AddContactView> {
|
|||
final ctrlrContact = TextEditingController(text: "");
|
||||
final ctrlrGroupName = TextEditingController(text: "");
|
||||
String server = "";
|
||||
// flutter textfield onChange often fires twice and since we need contexts, we can't easily use a controler/listener
|
||||
String lastContactValue = "";
|
||||
bool failedImport = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -52,9 +59,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 +70,10 @@ class _AddContactViewState extends State<AddContactView> {
|
|||
child: TabBarView(
|
||||
children: (groupsEnabled
|
||||
? [
|
||||
addPeerTab(),
|
||||
addGroupTab(),
|
||||
addPeerTab(bcontext),
|
||||
addGroupTab(bcontext),
|
||||
]
|
||||
: [addPeerTab()]),
|
||||
: [addPeerTab(bcontext)]),
|
||||
)),
|
||||
]));
|
||||
});
|
||||
|
@ -105,7 +113,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,
|
||||
|
@ -125,7 +133,7 @@ class _AddContactViewState extends State<AddContactView> {
|
|||
onPressed: _copyOnion,
|
||||
readonly: true,
|
||||
icon: Icon(
|
||||
CwtchIcons.address_copy_2,
|
||||
CwtchIcons.address_copy,
|
||||
size: 32,
|
||||
),
|
||||
tooltip: AppLocalizations.of(context)!.copyBtn,
|
||||
|
@ -139,34 +147,34 @@ class _AddContactViewState extends State<AddContactView> {
|
|||
),
|
||||
CwtchTextField(
|
||||
testKey: Key("txtAddP2P"),
|
||||
key: Key("txtAddP2P"),
|
||||
controller: ctrlrContact,
|
||||
validator: (value) {
|
||||
if (value == "") {
|
||||
return null;
|
||||
}
|
||||
if (globalErrorHandler.invalidImportStringError) {
|
||||
if (failedImport) {
|
||||
return AppLocalizations.of(context)!.invalidImportString;
|
||||
} else if (globalErrorHandler.contactAlreadyExistsError) {
|
||||
return AppLocalizations.of(context)!.contactAlreadyExists;
|
||||
} else if (globalErrorHandler.explicitAddContactSuccess) {}
|
||||
}
|
||||
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:", ""));
|
||||
|
||||
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 (lastContactValue != importBundle) {
|
||||
lastContactValue = importBundle;
|
||||
var profileOnion = Provider.of<ProfileInfoState>(bcontext, listen: false).onion;
|
||||
Provider.of<FlwtchState>(bcontext, listen: false).cwtch.ImportBundle(profileOnion, importBundle.replaceFirst("cwtch:", "")).then((result) {
|
||||
if (result == "importBundle.success") {
|
||||
failedImport = false;
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
failedImport = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
hintText: '',
|
||||
)
|
||||
|
@ -174,10 +182,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 +213,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 +225,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 +240,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 +253,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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cwtch/config.dart';
|
||||
import 'package:cwtch/controllers/filesharing.dart';
|
||||
import 'package:cwtch/cwtch/cwtch.dart';
|
||||
import 'package:cwtch/models/appstate.dart';
|
||||
import 'package:cwtch/models/profile.dart';
|
||||
|
@ -86,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>[
|
||||
|
@ -126,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,
|
||||
|
@ -158,7 +162,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
onPressed: _copyOnion,
|
||||
readonly: true,
|
||||
icon: Icon(
|
||||
CwtchIcons.address_copy_2,
|
||||
CwtchIcons.address_copy,
|
||||
size: 32,
|
||||
),
|
||||
tooltip: AppLocalizations.of(context)!.copyBtn,
|
||||
|
@ -272,36 +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, 75),
|
||||
maximumSize: Size(800, 75),
|
||||
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)!.enterCurrentPasswordForDelete,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
showAlertDialog(context);
|
||||
},
|
||||
icon: Icon(Icons.delete_forever),
|
||||
label: Text(AppLocalizations.of(context)!.deleteBtn),
|
||||
))
|
||||
]))
|
||||
child: Tooltip(
|
||||
message: AppLocalizations.of(context)!.exportProfileTooltip,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size(400, 75),
|
||||
maximumSize: Size(800, 75),
|
||||
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: Tooltip(
|
||||
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size(400, 75),
|
||||
maximumSize: Size(800, 75),
|
||||
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),
|
||||
)))
|
||||
]))))));
|
||||
});
|
||||
});
|
||||
|
@ -309,7 +348,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
|
||||
void _copyOnion() {
|
||||
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
|
||||
// TODO Toast
|
||||
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
|
||||
void _createPressed() async {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cwtch/cwtch/cwtch.dart';
|
||||
import 'package:cwtch/cwtch_icons_icons.dart';
|
||||
import 'package:cwtch/models/appstate.dart';
|
||||
import 'package:cwtch/models/contact.dart';
|
||||
|
@ -11,13 +14,17 @@ import 'package:cwtch/widgets/profileimage.dart';
|
|||
import 'package:cwtch/widgets/textfield.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import '../main.dart';
|
||||
import '../settings.dart';
|
||||
import 'addcontactview.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import 'messageview.dart';
|
||||
|
||||
enum ShareMenu { copyCode, qrcode }
|
||||
|
||||
class ContactsView extends StatefulWidget {
|
||||
const ContactsView({Key? key}) : super(key: key);
|
||||
|
||||
|
@ -27,26 +34,39 @@ class ContactsView extends StatefulWidget {
|
|||
|
||||
// selectConversation can be called from anywhere to set the active conversation
|
||||
void selectConversation(BuildContext context, int handle) {
|
||||
if (handle == Provider.of<AppState>(context, listen: false).selectedConversation) {
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
.SetConversationAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, handle, LastMessageSeenTimeKey, DateTime.now().toUtc().toIso8601String());
|
||||
}
|
||||
|
||||
void _pushMessageView(BuildContext context, int handle) {
|
||||
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext builderContext) {
|
||||
// assert we have an actual profile...
|
||||
// We need to listen for updates to the profile in order to update things like invitation message bubbles.
|
||||
PageRouteBuilder(
|
||||
settings: RouteSettings(name: "messages"),
|
||||
pageBuilder: (builderContext, a1, a2) {
|
||||
var profile = Provider.of<FlwtchState>(builderContext).profs.getProfile(profileOnion)!;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
|
@ -56,6 +76,8 @@ void _pushMessageView(BuildContext context, int handle) {
|
|||
builder: (context, child) => MessageView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -76,38 +98,45 @@ class _ContactsViewState extends State<ContactsView> {
|
|||
endDrawerEnableOpenDragGesture: false,
|
||||
drawerEnableOpenDragGesture: false,
|
||||
appBar: AppBar(
|
||||
leading: Row(children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: () {
|
||||
Provider.of<ProfileInfoState>(context, listen: false).recountUnread();
|
||||
Provider.of<AppState>(context, listen: false).selectedProfile = "";
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
StreamBuilder<bool>(
|
||||
stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
|
||||
builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
|
||||
int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "");
|
||||
leading: Stack(children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.arrow_back),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: () {
|
||||
Provider.of<ProfileInfoState>(context, listen: false).recountUnread();
|
||||
Provider.of<AppState>(context, listen: false).selectedProfile = "";
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
Positioned(
|
||||
bottom: 5.0,
|
||||
right: 5.0,
|
||||
child: StreamBuilder<bool>(
|
||||
stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
|
||||
builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
|
||||
int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "");
|
||||
|
||||
return Visibility(
|
||||
visible: unreadCount > 0,
|
||||
child: CircleAvatar(
|
||||
radius: 10.0,
|
||||
backgroundColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor,
|
||||
child: Text(unreadCount > 99 ? "99+" : unreadCount.toString(), style: TextStyle(color: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor, fontSize: 8.0)),
|
||||
));
|
||||
}),
|
||||
return Visibility(
|
||||
visible: unreadCount > 0,
|
||||
child: CircleAvatar(
|
||||
radius: 10.0,
|
||||
backgroundColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor,
|
||||
child: Text(unreadCount > 99 ? "99+" : unreadCount.toString(), style: TextStyle(color: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor, fontSize: 8.0)),
|
||||
));
|
||||
}),
|
||||
)
|
||||
]),
|
||||
title: RepaintBoundary(
|
||||
child: Row(children: [
|
||||
title: Row(children: [
|
||||
ProfileImage(
|
||||
imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
|
||||
? Provider.of<ProfileInfoState>(context).imagePath
|
||||
: Provider.of<ProfileInfoState>(context).defaultImagePath,
|
||||
diameter: 42,
|
||||
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
|
||||
border: Provider.of<ProfileInfoState>(context).isOnline
|
||||
? Provider.of<Settings>(context).current().portraitOnlineBorderColor
|
||||
: Provider.of<Settings>(context).current().portraitOfflineBorderColor,
|
||||
badgeTextColor: Colors.red,
|
||||
badgeColor: Colors.red,
|
||||
),
|
||||
|
@ -117,13 +146,16 @@ class _ContactsViewState extends State<ContactsView> {
|
|||
Expanded(
|
||||
child: Text("%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname).replaceAll("%2", AppLocalizations.of(context)!.titleManageContacts),
|
||||
overflow: TextOverflow.ellipsis, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor))),
|
||||
])),
|
||||
]),
|
||||
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());
|
||||
}
|
||||
|
@ -134,14 +166,49 @@ class _ContactsViewState extends State<ContactsView> {
|
|||
actions.add(Tooltip(message: AppLocalizations.of(context)!.blockUnknownConnectionsEnabledDescription, child: Icon(CwtchIcons.block_unknown)));
|
||||
}
|
||||
|
||||
// Copy profile onion
|
||||
actions.add(IconButton(
|
||||
icon: Icon(CwtchIcons.address_copy_2),
|
||||
tooltip: AppLocalizations.of(context)!.copyAddress,
|
||||
if (Provider.of<Settings>(context, listen: false).isExperimentEnabled(QRCodeExperiment)) {
|
||||
actions.add(PopupMenuButton<ShareMenu>(
|
||||
icon: Icon(CwtchIcons.address_copy),
|
||||
tooltip: AppLocalizations.of(context)!.shareProfileMenuTooltop,
|
||||
splashRadius: Material.defaultSplashRadius / 2,
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
|
||||
}));
|
||||
onSelected: (ShareMenu item) {
|
||||
switch (item) {
|
||||
case ShareMenu.copyCode:
|
||||
{
|
||||
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
|
||||
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
break;
|
||||
case ShareMenu.qrcode:
|
||||
{
|
||||
_showQRCode("cwtch:" + Provider.of<ProfileInfoState>(context, listen: false).onion);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<ShareMenu>>[
|
||||
PopupMenuItem<ShareMenu>(
|
||||
value: ShareMenu.copyCode,
|
||||
child: Text(AppLocalizations.of(context)!.copyAddress),
|
||||
),
|
||||
PopupMenuItem<ShareMenu>(
|
||||
value: ShareMenu.qrcode,
|
||||
child: Text(AppLocalizations.of(context)!.shareMenuQRCode),
|
||||
),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
actions.add(IconButton(
|
||||
icon: Icon(CwtchIcons.address_copy),
|
||||
tooltip: AppLocalizations.of(context)!.copyAddress,
|
||||
splashRadius: Material.defaultSplashRadius / 2,
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
|
||||
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}));
|
||||
}
|
||||
|
||||
// Manage known Servers
|
||||
if (Provider.of<Settings>(context, listen: false).isExperimentEnabled(TapirGroupsExperiment) || Provider.of<Settings>(context, listen: false).isExperimentEnabled(ServerManagementExperiment)) {
|
||||
|
@ -186,39 +253,188 @@ class _ContactsViewState extends State<ContactsView> {
|
|||
ChangeNotifierProvider.value(value: contact),
|
||||
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).serverList),
|
||||
],
|
||||
builder: (context, child) => RepaintBoundary(child: ContactRow()),
|
||||
builder: (context, child) => ContactRow(),
|
||||
);
|
||||
});
|
||||
|
||||
final divided = ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: tiles,
|
||||
).toList();
|
||||
return RepaintBoundary(child: ListView(children: divided));
|
||||
var initialScroll =
|
||||
Provider.of<ProfileInfoState>(context, listen: false).contactList.filteredList().indexWhere((element) => element.identifier == Provider.of<AppState>(context).selectedConversation);
|
||||
if (initialScroll < 0) {
|
||||
initialScroll = 0;
|
||||
}
|
||||
|
||||
var contactList = ScrollablePositionedList.separated(
|
||||
itemScrollController: Provider.of<ProfileInfoState>(context).contactListScrollController,
|
||||
itemCount: Provider.of<ContactListState>(context).numFiltered,
|
||||
initialScrollIndex: initialScroll,
|
||||
shrinkWrap: true,
|
||||
physics: BouncingScrollPhysics(),
|
||||
semanticChildCount: Provider.of<ContactListState>(context).numFiltered,
|
||||
itemBuilder: (context, index) {
|
||||
return tiles.elementAt(index);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return Divider(height: 1);
|
||||
},
|
||||
);
|
||||
|
||||
return contactList;
|
||||
}
|
||||
|
||||
void _pushAddContact() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext bcontext) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
|
||||
],
|
||||
child: AddContactView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
void _pushAddContact(bool newGroup) {
|
||||
// close modal
|
||||
Navigator.popUntil(context, (route) => route.settings.name == "conversations");
|
||||
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
settings: RouteSettings(name: "addcontact"),
|
||||
pageBuilder: (builderContext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
|
||||
],
|
||||
child: AddContactView(newGroup: newGroup),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _pushServers() {
|
||||
var profile = Provider.of<ProfileInfoState>(context);
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (bcontext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider(create: (context) => profile), Provider.value(value: Provider.of<FlwtchState>(context))],
|
||||
child: ProfileServersView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: Platform.isAndroid ? 250 : 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(399),
|
||||
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(399),
|
||||
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(399),
|
||||
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,
|
||||
),
|
||||
],
|
||||
))),
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
void _showQRCode(String profile_code) {
|
||||
showModalBottomSheet<dynamic>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider(create: (context) => profile), Provider.value(value: Provider.of<FlwtchState>(context))],
|
||||
child: ProfileServersView(),
|
||||
);
|
||||
return Wrap(children: <Widget>[
|
||||
Center(
|
||||
child: QrImage(
|
||||
data: profile_code,
|
||||
version: QrVersions.auto,
|
||||
size: 400.0,
|
||||
backgroundColor: Provider.of<Settings>(context).theme.backgroundPaneColor,
|
||||
foregroundColor: Provider.of<Settings>(context).theme.mainTextColor,
|
||||
gapless: false,
|
||||
),
|
||||
)
|
||||
]);
|
||||
},
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cwtch/config.dart';
|
||||
import 'package:cwtch/cwtch/cwtch.dart';
|
||||
import 'package:cwtch/main.dart';
|
||||
import 'package:cwtch/models/appstate.dart';
|
||||
import 'package:cwtch/models/contact.dart';
|
||||
import 'package:cwtch/models/profile.dart';
|
||||
import 'package:cwtch/settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import '../cwtch_icons_icons.dart';
|
||||
|
||||
class FileSharingView extends StatefulWidget {
|
||||
@override
|
||||
_FileSharingViewState createState() => _FileSharingViewState();
|
||||
}
|
||||
|
||||
class _FileSharingViewState extends State<FileSharingView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var handle = Provider.of<ContactInfoState>(context).nickname;
|
||||
if (handle.isEmpty) {
|
||||
handle = Provider.of<ContactInfoState>(context).onion;
|
||||
}
|
||||
|
||||
var profileHandle = Provider.of<ProfileInfoState>(context).onion;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(handle + " » " + AppLocalizations.of(context)!.manageSharedFiles),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: Provider.of<FlwtchState>(context, listen: false).cwtch.GetSharedFiles(profileHandle, Provider.of<ContactInfoState>(context).identifier),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
List<dynamic> sharedFiles = jsonDecode(snapshot.data as String);
|
||||
sharedFiles.sort((a, b) {
|
||||
return a["DateShared"].toString().compareTo(b["DateShared"].toString());
|
||||
});
|
||||
|
||||
var fileList = ScrollablePositionedList.separated(
|
||||
itemScrollController: ItemScrollController(),
|
||||
itemCount: sharedFiles.length,
|
||||
shrinkWrap: true,
|
||||
physics: BouncingScrollPhysics(),
|
||||
semanticChildCount: sharedFiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
String filekey = sharedFiles[index]["FileKey"];
|
||||
EnvironmentConfig.debugLog("$sharedFiles " + sharedFiles[index].toString());
|
||||
return SwitchListTile(
|
||||
title: Text(sharedFiles[index]["Path"]),
|
||||
subtitle: Text(sharedFiles[index]["DateShared"]),
|
||||
value: sharedFiles[index]["Active"],
|
||||
activeTrackColor: Provider.of<Settings>(context).theme.defaultButtonColor,
|
||||
inactiveTrackColor: Provider.of<Settings>(context).theme.defaultButtonDisabledColor,
|
||||
secondary: Icon(CwtchIcons.attached_file_2, color: Provider.of<Settings>(context).current().mainTextColor),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
if (newValue) {
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.RestartSharing(profileHandle, filekey);
|
||||
} else {
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.StopSharing(profileHandle, filekey);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return Divider(height: 1);
|
||||
},
|
||||
);
|
||||
return fileList;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
@ -168,28 +222,66 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
|||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
|
||||
secondary: Icon(CwtchIcons.streamer_bunnymask, color: settings.current().mainTextColor),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.formattingExperiment, style: TextStyle(color: settings.current().mainTextColor)),
|
||||
subtitle: Text(AppLocalizations.of(context)!.messageFormattingDescription),
|
||||
value: settings.isExperimentEnabled(FormattingExperiment),
|
||||
onChanged: (bool value) {
|
||||
if (value) {
|
||||
settings.enableExperiment(FormattingExperiment);
|
||||
} else {
|
||||
settings.disableExperiment(FormattingExperiment);
|
||||
}
|
||||
saveSettings(context);
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor,
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
|
||||
secondary: Icon(Icons.text_fields, color: settings.current().mainTextColor),
|
||||
),
|
||||
SizedBox(
|
||||
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(
|
||||
|
@ -301,7 +393,12 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
|||
value: settings.isExperimentEnabled(FileSharingExperiment),
|
||||
onChanged: (bool value) {
|
||||
if (value) {
|
||||
settings.enableExperiment(FileSharingExperiment);
|
||||
if (checkDownloadDirectory(context, settings)) {
|
||||
settings.enableExperiment(FileSharingExperiment);
|
||||
} else {
|
||||
settings.disableExperiment(FileSharingExperiment);
|
||||
settings.disableExperiment(ImagePreviewsExperiment);
|
||||
}
|
||||
} else {
|
||||
settings.disableExperiment(FileSharingExperiment);
|
||||
settings.disableExperiment(ImagePreviewsExperiment);
|
||||
|
@ -310,7 +407,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
|||
},
|
||||
activeTrackColor: settings.theme.defaultButtonColor,
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
|
||||
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor),
|
||||
secondary: Icon(CwtchIcons.attached_file_2, color: settings.current().mainTextColor),
|
||||
),
|
||||
Visibility(
|
||||
visible: settings.isExperimentEnabled(FileSharingExperiment),
|
||||
|
@ -321,8 +418,11 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
|||
value: settings.isExperimentEnabled(ImagePreviewsExperiment),
|
||||
onChanged: (bool value) {
|
||||
if (value) {
|
||||
settings.enableExperiment(ImagePreviewsExperiment);
|
||||
settings.downloadPath = Provider.of<FlwtchState>(context, listen: false).cwtch.defaultDownloadPath();
|
||||
if (checkDownloadDirectory(context, settings)) {
|
||||
settings.enableExperiment(ImagePreviewsExperiment);
|
||||
} else {
|
||||
settings.disableExperiment(ImagePreviewsExperiment);
|
||||
}
|
||||
} else {
|
||||
settings.disableExperiment(ImagePreviewsExperiment);
|
||||
}
|
||||
|
@ -330,7 +430,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,
|
||||
|
@ -368,6 +468,24 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
|||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
|
||||
secondary: Icon(Icons.link, color: settings.current().mainTextColor),
|
||||
)),
|
||||
Visibility(
|
||||
visible: settings.experimentsEnabled,
|
||||
child: SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.enableExperimentQRCode, style: TextStyle(color: settings.current().mainTextColor)),
|
||||
subtitle: Text(AppLocalizations.of(context)!.experimentQRCodeDescription),
|
||||
value: settings.isExperimentEnabled(QRCodeExperiment),
|
||||
onChanged: (bool value) {
|
||||
if (value) {
|
||||
settings.enableExperiment(QRCodeExperiment);
|
||||
} else {
|
||||
settings.disableExperiment(QRCodeExperiment);
|
||||
}
|
||||
saveSettings(context);
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor,
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
|
||||
secondary: Icon(Icons.qr_code, color: settings.current().mainTextColor),
|
||||
)),
|
||||
AboutListTile(
|
||||
icon: appIcon,
|
||||
applicationIcon: Padding(padding: EdgeInsets.all(5), child: Icon(CwtchIcons.cwtch_knott)),
|
||||
|
@ -380,10 +498,95 @@ 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: SwitchListTile(
|
||||
title: Text("Show Semantic Debugger", style: TextStyle(color: settings.current().mainTextColor)),
|
||||
subtitle: Text("Show Accessibility Debugging View"),
|
||||
value: settings.useSemanticDebugger,
|
||||
onChanged: (bool value) {
|
||||
if (value) {
|
||||
settings.useSemanticDebugger = value;
|
||||
} else {
|
||||
settings.useSemanticDebugger = value;
|
||||
}
|
||||
saveSettings(context);
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor,
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
|
||||
secondary: Icon(Icons.settings_accessibility, color: settings.current().mainTextColor),
|
||||
)),
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool checkDownloadDirectory(context, settings) {
|
||||
bool showError = false;
|
||||
if (settings.downloadPath != "") {
|
||||
} else {
|
||||
// check if the default download path exists
|
||||
var path = Provider.of<FlwtchState>(context, listen: false).cwtch.defaultDownloadPath();
|
||||
if (path != null) {
|
||||
settings.downloadPath = path;
|
||||
} else {
|
||||
showError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!showError && Directory(settings.downloadPath).existsSync()) {
|
||||
return true;
|
||||
} else {
|
||||
final snackBar = SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.errorDownloadDirectoryDoesNotExist),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a version string from Package Info
|
||||
|
@ -439,6 +642,12 @@ String getLanguageFull(context, String languageCode) {
|
|||
if (languageCode == "da") {
|
||||
return AppLocalizations.of(context)!.localeDa;
|
||||
}
|
||||
if (languageCode == "tr") {
|
||||
return AppLocalizations.of(context)!.localeTr;
|
||||
}
|
||||
if (languageCode == "nl") {
|
||||
return AppLocalizations.of(context)!.localeNl;
|
||||
}
|
||||
return languageCode;
|
||||
}
|
||||
|
||||
|
|
|
@ -91,22 +91,6 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
|
|||
tooltip: AppLocalizations.of(context)!.saveBtn,
|
||||
)
|
||||
]),
|
||||
|
||||
// Address Copy Button
|
||||
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchLabel(label: AppLocalizations.of(context)!.groupAddr),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchTextField(
|
||||
controller: ctrlrGroupAddr,
|
||||
hintText: '',
|
||||
validator: (value) {},
|
||||
)
|
||||
]),
|
||||
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
|
@ -183,7 +167,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,
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
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';
|
||||
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';
|
||||
|
@ -23,10 +27,12 @@ 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';
|
||||
import '../widgets/messagelist.dart';
|
||||
import 'filesharingview.dart';
|
||||
import 'groupsettingsview.dart';
|
||||
|
||||
class MessageView extends StatefulWidget {
|
||||
|
@ -39,8 +45,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() {
|
||||
|
@ -51,7 +58,14 @@ 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;
|
||||
}
|
||||
});
|
||||
ctrlrCompose.text = Provider.of<ContactInfoState>(context, listen: false).messageDraft ?? "";
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -78,17 +92,24 @@ 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 (showFileSharing) {
|
||||
appBarButtons.add(IconButton(
|
||||
splashRadius: Material.defaultSplashRadius / 2, icon: Icon(CwtchIcons.manage_files), tooltip: AppLocalizations.of(context)!.manageSharedFiles, onPressed: _pushFileSharingSettings));
|
||||
}
|
||||
|
||||
if (Provider.of<ContactInfoState>(context).isOnline()) {
|
||||
if (showFileSharing) {
|
||||
appBarButtons.add(IconButton(
|
||||
splashRadius: Material.defaultSplashRadius / 2,
|
||||
icon: Icon(Icons.attach_file, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor),
|
||||
icon: Icon(CwtchIcons.attached_file_2, size: 26, color: Provider.of<Settings>(context).theme.mainTextColor),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipSendFile,
|
||||
onPressed: Provider.of<AppState>(context).disableFilePicker
|
||||
? null
|
||||
|
@ -106,6 +127,7 @@ class _MessageViewState extends State<MessageView> {
|
|||
},
|
||||
));
|
||||
}
|
||||
|
||||
appBarButtons.add(IconButton(
|
||||
splashRadius: Material.defaultSplashRadius / 2,
|
||||
icon: Icon(CwtchIcons.send_invite, size: 24),
|
||||
|
@ -125,28 +147,61 @@ 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, listen: false).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)
|
||||
? Provider.of<ContactInfoState>(context).imagePath
|
||||
: Provider.of<ContactInfoState>(context).defaultImagePath,
|
||||
diameter: 42,
|
||||
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
|
||||
badgeTextColor: Colors.red,
|
||||
badgeColor: Colors.red,
|
||||
),
|
||||
imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
|
||||
? Provider.of<ContactInfoState>(context).imagePath
|
||||
: Provider.of<ContactInfoState>(context).defaultImagePath,
|
||||
diameter: 42,
|
||||
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
|
||||
badgeTextColor: Colors.red,
|
||||
badgeColor: Provider.of<Settings>(context).theme.portraitContactBadgeColor,
|
||||
badgeIcon: Provider.of<ContactInfoState>(context).isGroup
|
||||
? (Tooltip(
|
||||
message: Provider.of<ContactInfoState>(context).isOnline()
|
||||
? Provider.of<ContactInfoState>(context).antispamTickets == 0
|
||||
? AppLocalizations.of(context)!.acquiringTicketsFromServer
|
||||
: AppLocalizations.of(context)!.acquiredTicketsFromServer
|
||||
: AppLocalizations.of(context)!.serverConnectivityDisconnected,
|
||||
child: Provider.of<ContactInfoState>(context).isOnline()
|
||||
? Provider.of<ContactInfoState>(context).antispamTickets == 0
|
||||
? Icon(
|
||||
CwtchIcons.anti_spam_3,
|
||||
size: 14.0,
|
||||
semanticLabel: AppLocalizations.of(context)!.acquiringTicketsFromServer,
|
||||
color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
|
||||
)
|
||||
: Icon(
|
||||
CwtchIcons.anti_spam_2,
|
||||
color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
|
||||
size: 14.0,
|
||||
)
|
||||
: Icon(
|
||||
CwtchIcons.onion_off,
|
||||
color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
|
||||
size: 14.0,
|
||||
)))
|
||||
: null),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
|
@ -158,35 +213,74 @@ 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;
|
||||
}
|
||||
|
||||
void _pushFileSharingSettings() {
|
||||
var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
|
||||
var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (builderContext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
|
||||
child: FileSharingView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _pushContactSettings() {
|
||||
var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
|
||||
var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
|
||||
|
||||
if (Provider.of<ContactInfoState>(context, listen: false).isGroup == true) {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(builder: (BuildContext bcontext) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
|
||||
child: GroupSettingsView(),
|
||||
);
|
||||
}));
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (builderContext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
|
||||
child: GroupSettingsView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(builder: (BuildContext bcontext) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
|
||||
child: PeerSettingsView(),
|
||||
);
|
||||
}));
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (builderContext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: profileInfoState), ChangeNotifierProvider.value(value: contactInfoState)],
|
||||
child: PeerSettingsView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,9 +293,15 @@ class _MessageViewState extends State<MessageView> {
|
|||
void _sendMessage([String? ignoredParam]) {
|
||||
// Trim message
|
||||
final messageWithoutNewLine = ctrlrCompose.value.text.trimRight();
|
||||
ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine);
|
||||
ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine, selection: TextSelection.fromPosition(TextPosition(offset: messageWithoutNewLine.length)));
|
||||
|
||||
var isGroup = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(Provider.of<AppState>(context, listen: false).selectedConversation!)!.isGroup;
|
||||
// Do this after we trim to preserve enter-behaviour...
|
||||
bool isOffline = Provider.of<ContactInfoState>(context, listen: false).isOnline() == false;
|
||||
bool performingAntiSpam = Provider.of<ContactInfoState>(context).antispamTickets == 0;
|
||||
bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
|
||||
if (isOffline || (isGroup && performingAntiSpam)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// peers and groups currently have different length constraints (servers can store less)...
|
||||
var actualMessageLength = ctrlrCompose.value.text.length;
|
||||
|
@ -209,31 +309,32 @@ 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));
|
||||
ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
|
||||
} catch (e) {}
|
||||
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm))
|
||||
.then(_sendMessageHandler);
|
||||
} catch (e) {
|
||||
EnvironmentConfig.debugLog("Exception: reply to message could not be found: " + e.toString());
|
||||
}
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
||||
_sendMessageHelper();
|
||||
});
|
||||
} else {
|
||||
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
|
||||
_sendMessageHelper();
|
||||
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm))
|
||||
.then(_sendMessageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,89 +342,276 @@ class _MessageViewState extends State<MessageView> {
|
|||
void _sendInvitation([String? ignoredParam]) {
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact);
|
||||
_sendMessageHelper();
|
||||
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact)
|
||||
.then(_sendMessageHandler);
|
||||
}
|
||||
|
||||
void _sendFile(String filePath) {
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath);
|
||||
_sendMessageHelper();
|
||||
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath)
|
||||
.then(_sendMessageHandler);
|
||||
}
|
||||
|
||||
void _sendMessageHelper() {
|
||||
void _sendMessageHandler(dynamic messageJson) {
|
||||
if (Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0) {
|
||||
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.acquiringTicketsFromServer));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return;
|
||||
}
|
||||
|
||||
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
|
||||
var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
|
||||
var profile = Provider.of<ProfileInfoState>(context, listen: false);
|
||||
|
||||
var messageInfo = messageJsonToInfo(profileOnion, identifier, messageJson);
|
||||
if (messageInfo != null) {
|
||||
profile.newMessage(
|
||||
messageInfo.metadata.conversationIdentifier,
|
||||
messageInfo.metadata.messageID,
|
||||
messageInfo.metadata.timestamp,
|
||||
messageInfo.metadata.senderHandle,
|
||||
messageInfo.metadata.senderImage ?? "",
|
||||
messageInfo.metadata.isAuto,
|
||||
messageInfo.wrapper,
|
||||
messageInfo.metadata.contenthash,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
Provider.of<ContactInfoState>(context, listen: false).messageDraft = null;
|
||||
ctrlrCompose.clear();
|
||||
focusNode.requestFocus();
|
||||
Future.delayed(const Duration(milliseconds: 80), () {
|
||||
var profile = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
|
||||
var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
|
||||
fetchAndCacheMessageInfo(context, profile, identifier, ByIndex(0));
|
||||
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
|
||||
Provider.of<ContactInfoState>(context, listen: false).totalMessages += 1;
|
||||
// Resort the contact list...
|
||||
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).identifier, DateTime.now());
|
||||
});
|
||||
|
||||
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(
|
||||
tooltip: AppLocalizations.of(context)!.tooltipBackToMessageEditing,
|
||||
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: !isOffline,
|
||||
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),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipBoldText,
|
||||
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),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipItalicize,
|
||||
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),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipCode,
|
||||
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),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipSuperscript,
|
||||
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),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipSubscript,
|
||||
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),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipStrikethrough,
|
||||
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),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipPreviewFormatting,
|
||||
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) {
|
||||
Provider.of<ContactInfoState>(context, listen: false).messageDraft = 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: Tooltip(
|
||||
message: isOffline
|
||||
? (isGroup ? AppLocalizations.of(context)!.serverNotSynced : AppLocalizations.of(context)!.peerOfflineMessage)
|
||||
: (isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0)
|
||||
? AppLocalizations.of(context)!.acquiringTicketsFromServer
|
||||
: AppLocalizations.of(context)!.sendMessage,
|
||||
child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor)),
|
||||
onPressed: isOffline || (isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0) ? 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) {
|
||||
|
@ -332,6 +620,9 @@ class _MessageViewState extends State<MessageView> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var message = snapshot.data! as Message;
|
||||
var qTextColor = message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
|
||||
? Provider.of<Settings>(context).theme.messageFromOtherTextColor
|
||||
: Provider.of<Settings>(context).theme.messageFromMeTextColor;
|
||||
return Container(
|
||||
margin: EdgeInsets.all(5),
|
||||
padding: EdgeInsets.all(5),
|
||||
|
@ -340,6 +631,26 @@ class _MessageViewState extends State<MessageView> {
|
|||
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Stack(children: [
|
||||
Container(
|
||||
margin: EdgeInsets.all(5),
|
||||
padding: EdgeInsets.all(5),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
|
||||
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor
|
||||
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
|
||||
),
|
||||
height: 75,
|
||||
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)),
|
||||
Flexible(
|
||||
child: DefaultTextStyle(
|
||||
textWidthBasis: TextWidthBasis.parent,
|
||||
child: message.getPreviewWidget(context),
|
||||
style: TextStyle(color: qTextColor),
|
||||
overflow: TextOverflow.fade,
|
||||
))
|
||||
])),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
|
@ -350,16 +661,7 @@ class _MessageViewState extends State<MessageView> {
|
|||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
||||
},
|
||||
)),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(padding: EdgeInsets.all(2.0), child: Icon(Icons.reply)),
|
||||
)
|
||||
]),
|
||||
Wrap(
|
||||
runAlignment: WrapAlignment.spaceEvenly,
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 1.0,
|
||||
children: [Center(widthFactor: 1.0, child: Padding(padding: EdgeInsets.all(10.0), child: message.getPreviewWidget(context)))]),
|
||||
]));
|
||||
} else {
|
||||
return MessageLoadingBubble();
|
||||
|
@ -372,14 +674,20 @@ 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...
|
||||
void handleKeyPress(RawKeyEvent event) {
|
||||
var data = event.data;
|
||||
if ((data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) || data.logicalKey == LogicalKeyboardKey.numpadEnter && !event.isShiftPressed) {
|
||||
_sendMessage();
|
||||
if (event is RawKeyUpEvent) {
|
||||
if ((data.logicalKey == LogicalKeyboardKey.enter && !event.isShiftPressed) || data.logicalKey == LogicalKeyboardKey.numpadEnter && !event.isShiftPressed) {
|
||||
// Don't send when inserting a new line that is not at the end of the message
|
||||
if (ctrlrCompose.selection.baseOffset != ctrlrCompose.text.length) {
|
||||
return;
|
||||
}
|
||||
_sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -400,6 +708,7 @@ class _MessageViewState extends State<MessageView> {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(AppLocalizations.of(bcontext)!.invitationLabel),
|
||||
SizedBox(
|
||||
|
@ -408,10 +717,10 @@ class _MessageViewState extends State<MessageView> {
|
|||
ChangeNotifierProvider.value(
|
||||
value: Provider.of<ProfileInfoState>(ctx, listen: false),
|
||||
child: DropdownContacts(filter: (contact) {
|
||||
return contact.onion != Provider.of<ContactInfoState>(context).onion;
|
||||
return contact.onion != Provider.of<ContactInfoState>(ctx).onion;
|
||||
}, onChanged: (newVal) {
|
||||
setState(() {
|
||||
this.selectedContact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(newVal)!.identifier;
|
||||
this.selectedContact = Provider.of<ProfileInfoState>(ctx, listen: false).contactList.findContact(newVal)!.identifier;
|
||||
});
|
||||
})),
|
||||
SizedBox(
|
||||
|
|
|
@ -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),
|
||||
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,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cwtch/constants.dart';
|
||||
import 'package:cwtch/controllers/enter_password.dart';
|
||||
import 'package:cwtch/controllers/filesharing.dart';
|
||||
import 'package:cwtch/cwtch_icons_icons.dart';
|
||||
import 'package:cwtch/models/appstate.dart';
|
||||
import 'package:cwtch/models/profile.dart';
|
||||
|
@ -67,11 +70,12 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
actions: getActions(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _pushAddProfile,
|
||||
onPressed: _modalAddImportProfiles,
|
||||
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
|
||||
color: Provider.of<Settings>(context).theme.defaultButtonTextColor,
|
||||
),
|
||||
),
|
||||
body: _buildProfileManager(),
|
||||
|
@ -126,52 +130,153 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
}
|
||||
|
||||
void _pushGlobalSettings() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return Provider(
|
||||
create: (_) => Provider.of<FlwtchState>(context, listen: false),
|
||||
child: GlobalSettingsView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (bcontext, a1, a2) {
|
||||
return Provider(
|
||||
create: (_) => Provider.of<FlwtchState>(bcontext, listen: false),
|
||||
child: GlobalSettingsView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _pushServers() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
settings: RouteSettings(name: "servers"),
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
|
||||
child: ServersView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
settings: RouteSettings(name: "servers"),
|
||||
pageBuilder: (bcontext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
|
||||
child: ServersView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _pushTorStatus() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
|
||||
child: TorStatusView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
settings: RouteSettings(name: "torconfig"),
|
||||
pageBuilder: (bcontext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
|
||||
child: TorStatusView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _pushAddProfile({onion: ""}) {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ProfileInfoState>(
|
||||
create: (_) => ProfileInfoState(onion: onion),
|
||||
),
|
||||
],
|
||||
builder: (context, widget) => AddEditProfileView(key: Key('addprofile')),
|
||||
);
|
||||
},
|
||||
));
|
||||
void _pushAddProfile(bcontext, {onion: ""}) {
|
||||
Navigator.popUntil(bcontext, (route) => route.isFirst);
|
||||
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (bcontext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ProfileInfoState>(
|
||||
create: (_) => ProfileInfoState(onion: onion),
|
||||
),
|
||||
],
|
||||
builder: (context, widget) => AddEditProfileView(key: Key('addprofile')),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _modalAddImportProfiles() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: RepaintBoundary(
|
||||
child: Container(
|
||||
height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size(399, 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(399, 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(color: Provider.of<Settings>(context).theme.mainTextColor, 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, () {}, () {});
|
||||
},
|
||||
))),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
))),
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
void _modalUnlockProfiles() {
|
||||
|
@ -183,7 +288,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: RepaintBoundary(
|
||||
child: Container(
|
||||
height: 200, // bespoke value courtesy of the [TextField] docs
|
||||
height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
|
@ -235,7 +340,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
(ProfileInfoState profile) {
|
||||
return ChangeNotifierProvider<ProfileInfoState>.value(
|
||||
value: profile,
|
||||
builder: (context, child) => RepaintBoundary(child: ProfileRow()),
|
||||
builder: (context, child) => ProfileRow(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ class _ProfileServersView extends State<ProfileServersView> {
|
|||
(RemoteServerInfoState server) {
|
||||
return ChangeNotifierProvider<RemoteServerInfoState>.value(
|
||||
value: server,
|
||||
builder: (context, child) => RepaintBoundary(child: RemoteServerRow()),
|
||||
builder: (context, child) => RemoteServerRow(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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>(
|
||||
|
@ -140,17 +141,21 @@ class _ServersView extends State<ServersView> {
|
|||
}
|
||||
|
||||
void _pushAddServer() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ServerInfoState>(
|
||||
create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true),
|
||||
)
|
||||
],
|
||||
child: AddEditServerView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (bcontext, a1, a2) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ServerInfoState>(
|
||||
create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true),
|
||||
)
|
||||
],
|
||||
child: AddEditServerView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,11 +49,13 @@ class _SplashViewState extends State<SplashView> {
|
|||
? appState.appError
|
||||
: appState.modalState == ModalState.none
|
||||
? AppLocalizations.of(context)!.loadingCwtch
|
||||
: AppLocalizations.of(context)!.storageMigrationModalMessage,
|
||||
: appState.modalState == ModalState.storageMigration
|
||||
? AppLocalizations.of(context)!.storageMigrationModalMessage
|
||||
: AppLocalizations.of(context)!.shuttingDownApp, // Todo l10n AppLocalizations.of(context)!.storageMigrationModalMessage
|
||||
style: TextStyle(
|
||||
fontSize: 16.0, color: appState.appError == "" ? Provider.of<Settings>(context).theme.mainTextColor : Provider.of<Settings>(context).theme.textfieldErrorColor))),
|
||||
Visibility(
|
||||
visible: appState.modalState == ModalState.storageMigration,
|
||||
visible: appState.modalState == ModalState.storageMigration || appState.modalState == ModalState.shutdown,
|
||||
child: LinearProgressIndicator(
|
||||
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
|
||||
))
|
||||
|
|
|
@ -29,6 +29,7 @@ class _DropdownContactsState extends State<DropdownContacts> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButton(
|
||||
isExpanded: true, // magic property
|
||||
value: this.selected,
|
||||
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.where(widget.filter).map<DropdownMenuItem<String>>((ContactInfoState contact) {
|
||||
return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname));
|
||||
|
|
|
@ -2,12 +2,10 @@ import 'dart:io';
|
|||
|
||||
import 'package:cwtch/models/appstate.dart';
|
||||
import 'package:cwtch/models/contact.dart';
|
||||
import 'package:cwtch/models/contactlist.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';
|
||||
|
@ -21,6 +19,8 @@ class ContactRow extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ContactRowState extends State<ContactRow> {
|
||||
bool isHover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var contact = Provider.of<ContactInfoState>(context);
|
||||
|
@ -37,12 +37,11 @@ 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,
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: InkWell(
|
||||
return InkWell(
|
||||
enableFeedback: true,
|
||||
splashFactory: InkSplash.splashFactory,
|
||||
child: Ink(
|
||||
color: Provider.of<AppState>(context).selectedConversation == contact.identifier ? Provider.of<Settings>(context).theme.backgroundHilightElementColor : Colors.transparent,
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0), //border size
|
||||
|
@ -80,49 +79,84 @@ 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);
|
||||
},
|
||||
));
|
||||
Visibility(
|
||||
// only allow pinning for non-blocked and accepted conversations,
|
||||
visible: contact.isAccepted() && (Platform.isAndroid || (!Platform.isAndroid && isHover) || contact.pinned),
|
||||
child: IconButton(
|
||||
tooltip: contact.pinned ? AppLocalizations.of(context)!.tooltipUnpinConversation : AppLocalizations.of(context)!.tooltipPinConversation,
|
||||
icon: Icon(
|
||||
contact.pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||
color: Provider.of<Settings>(context).theme.mainTextColor,
|
||||
),
|
||||
onPressed: () {
|
||||
if (contact.pinned) {
|
||||
contact.unpin(context);
|
||||
} else {
|
||||
contact.pin(context);
|
||||
}
|
||||
Provider.of<ContactListState>(context, listen: false).resort();
|
||||
},
|
||||
))
|
||||
])),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectConversation(context, contact.identifier);
|
||||
});
|
||||
},
|
||||
onHover: (hover) {
|
||||
if (isHover != hover) {
|
||||
setState(() {
|
||||
isHover = hover;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _btnApprove() {
|
||||
|
@ -148,7 +182,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.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cwtch/config.dart';
|
||||
import 'package:cwtch/models/contact.dart';
|
||||
|
@ -25,8 +26,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,19 +46,35 @@ 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: min(MediaQuery.of(context).size.height * 0.30, 150),
|
||||
isAntiAlias: false,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return MalformedBubble();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var fromMe = Provider.of<MessageMetadata>(context, listen: false).senderHandle == Provider.of<ProfileInfoState>(context).onion;
|
||||
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 showFileSharing = Provider.of<Settings>(context, listen: false).isExperimentEnabled(FileSharingExperiment);
|
||||
var borderRadius = 15.0;
|
||||
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
|
||||
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
|
||||
|
||||
var metadata = Provider.of<MessageMetadata>(context);
|
||||
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);
|
||||
|
@ -69,9 +87,24 @@ class FileBubbleState extends State<FileBubble> {
|
|||
if (downloadComplete && path != null) {
|
||||
var lpath = path.toLowerCase();
|
||||
if (lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp")) {
|
||||
if (myFile == null) {
|
||||
if (myFile == null || myFile?.path != path) {
|
||||
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 +127,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,
|
||||
|
@ -115,24 +154,11 @@ class FileBubbleState extends State<FileBubble> {
|
|||
if (Provider.of<Settings>(context).shouldPreview(path)) {
|
||||
isPreview = true;
|
||||
wdgDecorations = Center(
|
||||
widthFactor: 1.0,
|
||||
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 +193,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 +213,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(
|
||||
|
@ -197,7 +225,18 @@ class FileBubbleState extends State<FileBubble> {
|
|||
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [wdgSender, isPreview ? Container() : wdgMessage, wdgDecorations, messageStatusWidget]),
|
||||
children: [
|
||||
wdgSender,
|
||||
isPreview
|
||||
? Container(
|
||||
width: 0,
|
||||
padding: EdgeInsets.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
)
|
||||
: wdgMessage,
|
||||
wdgDecorations,
|
||||
messageStatusWidget
|
||||
]),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
@ -227,7 +266,7 @@ class FileBubbleState extends State<FileBubble> {
|
|||
var manifestPath = file.path + ".manifest";
|
||||
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
|
||||
ContactInfoState? contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
|
||||
ContactInfoState? contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle);
|
||||
if (contact != null) {
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
|
||||
}
|
||||
|
@ -356,7 +395,7 @@ class FileBubbleState extends State<FileBubble> {
|
|||
void pop(context, File myFile, Widget meta) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
builder: (bcontext) => Dialog(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
|
@ -365,16 +404,16 @@ class FileBubbleState extends State<FileBubble> {
|
|||
title: meta,
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
color: Provider.of<Settings>(context, listen: false).theme.toolbarIconColor,
|
||||
color: Provider.of<Settings>(bcontext, listen: false).theme.toolbarIconColor,
|
||||
iconSize: 32,
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
Navigator.pop(bcontext, true);
|
||||
})),
|
||||
Image.file(
|
||||
myFile,
|
||||
cacheWidth: (MediaQuery.of(context).size.width * 0.6).floor(),
|
||||
width: (MediaQuery.of(context).size.width * 0.6),
|
||||
height: (MediaQuery.of(context).size.height * 0.6),
|
||||
cacheWidth: (MediaQuery.of(bcontext).size.width * 0.6).floor(),
|
||||
width: (MediaQuery.of(bcontext).size.width * 0.6),
|
||||
height: (MediaQuery.of(bcontext).size.height * 0.6),
|
||||
fit: BoxFit.scaleDown,
|
||||
),
|
||||
SizedBox(
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:cwtch/config.dart';
|
||||
import 'package:cwtch/controllers/filesharing.dart';
|
||||
import 'package:cwtch/cwtch_icons_icons.dart';
|
||||
import 'package:cwtch/models/appstate.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'dart:io';
|
||||
|
@ -43,27 +46,25 @@ class _CwtchFolderPickerState extends State<CwtchFolderPicker> {
|
|||
testKey: widget.testKey,
|
||||
controller: ctrlrVal,
|
||||
readonly: Platform.isAndroid,
|
||||
onPressed: () async {
|
||||
if (Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
onPressed: Provider.of<AppState>(context).disableFilePicker
|
||||
? null
|
||||
: () async {
|
||||
if (Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var selectedDirectory = await getDirectoryPath();
|
||||
if (selectedDirectory != null) {
|
||||
//File directory = File(selectedDirectory);
|
||||
selectedDirectory += "/";
|
||||
ctrlrVal.text = selectedDirectory;
|
||||
if (widget.onSave != null) {
|
||||
widget.onSave!(selectedDirectory);
|
||||
}
|
||||
} else {
|
||||
// User canceled the picker
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
},
|
||||
var selectedDirectory = await showSelectDirectoryPicker(context);
|
||||
if (selectedDirectory != null) {
|
||||
//File directory = File(selectedDirectory);
|
||||
selectedDirectory += "/";
|
||||
ctrlrVal.text = selectedDirectory;
|
||||
if (widget.onSave != null) {
|
||||
widget.onSave!(selectedDirectory);
|
||||
}
|
||||
} else {
|
||||
// User canceled the picker
|
||||
}
|
||||
},
|
||||
onChanged: widget.onSave,
|
||||
icon: Icon(Icons.folder),
|
||||
tooltip: widget.tooltip,
|
||||
|
|
|
@ -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';
|
||||
|
@ -32,7 +29,7 @@ class MessageBubbleState extends State<MessageBubble> {
|
|||
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
|
||||
var borderRadiousEh = 15.0;
|
||||
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
|
||||
|
||||
var formatMessages = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
|
||||
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
|
||||
|
||||
// If the sender is not us, then we want to give them a nickname...
|
||||
|
@ -48,40 +45,29 @@ class MessageBubbleState extends State<MessageBubble> {
|
|||
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;
|
||||
|
||||
if (!showClickableLinks) {
|
||||
wdgMessage = SelectableText(
|
||||
widget.content + '\u202F',
|
||||
//key: Key(myKey),
|
||||
focusNode: _focus,
|
||||
style: TextStyle(
|
||||
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
);
|
||||
} else {
|
||||
wdgMessage = SelectableLinkify(
|
||||
text: widget.content + '\u202F',
|
||||
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
|
||||
options: LinkifyOptions(looseUrl: true, defaultToHttps: true),
|
||||
linkifiers: [UrlLinkifier()],
|
||||
onOpen: (link) {
|
||||
_modalOpenLink(context, link);
|
||||
},
|
||||
//key: Key(myKey),
|
||||
focusNode: _focus,
|
||||
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,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
);
|
||||
}
|
||||
var wdgMessage = SelectableLinkify(
|
||||
text: widget.content + '\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,
|
||||
);
|
||||
|
||||
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, messageDate: messageDate);
|
||||
|
||||
|
@ -115,57 +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(
|
||||
"Opening this link will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open links from people you trust. Are you sure you want to continue?"),
|
||||
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
|
||||
child: ElevatedButton(
|
||||
child: Text("Copy link", semanticsLabel: "Copy link"),
|
||||
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("Open link", semanticsLabel: "Open link"),
|
||||
onPressed: () async {
|
||||
if (await canLaunch(link.url)) {
|
||||
await launch(link.url);
|
||||
Navigator.pop(bcontext);
|
||||
} else {
|
||||
throw 'Could not launch $link';
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
)),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,18 +23,27 @@ 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";
|
||||
bool isGroupAndSynced = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status == "Synced";
|
||||
bool isGroupAndNotAuthenticated = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status != "Authenticated";
|
||||
|
||||
// Older checks, no longer used, kept for reference.
|
||||
//bool isGroupAndSynced = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status == "Synced";
|
||||
//bool isGroupAndNotAuthenticated = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status != "Authenticated";
|
||||
|
||||
bool showEphemeralWarning = (isP2P && Provider.of<ContactInfoState>(context).savePeerHistory != "SaveHistory");
|
||||
bool showOfflineWarning = Provider.of<ContactInfoState>(context).isOnline() == false;
|
||||
bool showSyncing = isGroupAndSyncing;
|
||||
bool showMessageWarning = showEphemeralWarning || showOfflineWarning || showSyncing;
|
||||
// Only load historical messages when the conversation is with a p2p contact OR the conversation is a server and *not* syncing.
|
||||
bool loadMessages = isP2P || (isGroupAndSynced || isGroupAndNotAuthenticated);
|
||||
// We used to only load historical messages when the conversation is with a p2p contact OR the conversation is a server and *not* syncing.
|
||||
// With the message cache in place this is no longer necessary
|
||||
bool loadMessages = true;
|
||||
|
||||
return RepaintBoundary(
|
||||
child: Container(
|
||||
|
@ -73,25 +83,25 @@ 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...
|
||||
itemBuilder: (itemBuilderContext, index) {
|
||||
var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
|
||||
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).identifier;
|
||||
var profileOnion = Provider.of<ProfileInfoState>(itemBuilderContext, listen: false).onion;
|
||||
var contactHandle = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).identifier;
|
||||
var messageIndex = index;
|
||||
|
||||
return FutureBuilder(
|
||||
future: messageHandler(outerContext, profileOnion, contactHandle, ByIndex(messageIndex)),
|
||||
future: messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex)),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var message = snapshot.data as Message;
|
||||
// here we create an index key for the contact and assign it to the row. Indexes are unique so we can
|
||||
// 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);
|
||||
var key = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).getMessageKey(contactHandle, messageIndex);
|
||||
return message.getWidget(context, key, messageIndex);
|
||||
} else {
|
||||
return MessageLoadingBubble();
|
||||
}
|
||||
|
|
|
@ -7,19 +7,23 @@ import 'package:cwtch/models/contact.dart';
|
|||
import 'package:cwtch/models/message.dart';
|
||||
import 'package:cwtch/models/profile.dart';
|
||||
import 'package:cwtch/views/contactsview.dart';
|
||||
import 'package:cwtch/widgets/staticmessagebubble.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cwtch/widgets/profileimage.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../models/messagecache.dart';
|
||||
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 +36,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(() {
|
||||
|
@ -75,7 +76,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
}
|
||||
}
|
||||
|
||||
Widget wdgIcons = Platform.isAndroid
|
||||
Widget wdgReply = Platform.isAndroid
|
||||
? SizedBox.shrink()
|
||||
: Visibility(
|
||||
visible: EnvironmentConfig.TEST_MODE || Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
|
||||
|
@ -91,13 +92,37 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
},
|
||||
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor)));
|
||||
|
||||
var settings = Provider.of<Settings>(context);
|
||||
var pis = Provider.of<ProfileInfoState>(context);
|
||||
var cis = Provider.of<ContactInfoState>(context);
|
||||
var borderColor = Provider.of<Settings>(context).theme.portraitOnlineBorderColor;
|
||||
var messageID = Provider.of<MessageMetadata>(context).messageID;
|
||||
var cache = Provider.of<ContactInfoState>(context).messageCache;
|
||||
|
||||
Widget wdgSeeReplies = Platform.isAndroid
|
||||
? SizedBox.shrink()
|
||||
: Visibility(
|
||||
visible: EnvironmentConfig.TEST_MODE || Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
|
||||
maintainSize: true,
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
maintainInteractivity: false,
|
||||
child: IconButton(
|
||||
tooltip: AppLocalizations.of(context)!.viewReplies,
|
||||
splashRadius: Material.defaultSplashRadius / 2,
|
||||
onPressed: () {
|
||||
modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
|
||||
},
|
||||
icon: Icon(CwtchIcons.view_replies, color: Provider.of<Settings>(context).theme.dropShadowColor)));
|
||||
|
||||
Widget wdgSpacer = Flexible(flex: 1, child: SizedBox(width: Platform.isAndroid ? 20 : 60, height: 10));
|
||||
var widgetRow = <Widget>[];
|
||||
|
||||
if (fromMe) {
|
||||
widgetRow = <Widget>[
|
||||
wdgSpacer,
|
||||
wdgIcons,
|
||||
wdgSeeReplies,
|
||||
wdgReply,
|
||||
actualMessage,
|
||||
];
|
||||
} else if (isBlocked && !showBlockedMessage) {
|
||||
|
@ -144,7 +169,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
});
|
||||
})),
|
||||
]))),
|
||||
wdgIcons,
|
||||
wdgReply,
|
||||
wdgSeeReplies,
|
||||
wdgSpacer,
|
||||
];
|
||||
} else {
|
||||
|
@ -180,7 +206,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
widgetRow = <Widget>[
|
||||
wdgPortrait,
|
||||
actualMessage,
|
||||
wdgIcons,
|
||||
wdgReply,
|
||||
wdgSeeReplies,
|
||||
wdgSpacer,
|
||||
];
|
||||
}
|
||||
|
@ -213,6 +240,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
_runAnimation(details.velocity.pixelsPerSecond, size);
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
|
||||
},
|
||||
onLongPress: () async {
|
||||
modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: Align(
|
||||
|
@ -223,10 +253,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: widgetRow,
|
||||
)))));
|
||||
var mark = Provider.of<ContactInfoState>(context).newMarker;
|
||||
if (mark > 0 &&
|
||||
Provider.of<ContactInfoState>(context).messageCache.indexedLength > mark &&
|
||||
Provider.of<ContactInfoState>(context).messageCache.getByIndex(mark - 1)?.metadata.messageID == 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;
|
||||
|
@ -278,6 +306,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
// Can't happen
|
||||
} else {
|
||||
selectConversation(context, id);
|
||||
var contactIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.filteredList().indexWhere((element) => element.identifier == id);
|
||||
Provider.of<ProfileInfoState>(context, listen: false).contactListScrollController.jumpTo(index: contactIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,3 +363,89 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
void modalShowReplies(
|
||||
BuildContext ctx, String replyHeader, String noRepliesText, Settings settings, ProfileInfoState profile, ContactInfoState cis, Color borderColor, MessageCache cache, int messageID,
|
||||
{bool showImage = true}) {
|
||||
showModalBottomSheet<void>(
|
||||
context: ctx,
|
||||
builder: (BuildContext bcontext) {
|
||||
List<Message> replies = getReplies(cache, messageID);
|
||||
|
||||
return ChangeNotifierProvider.value(
|
||||
value: profile,
|
||||
builder: (bcontext, child) {
|
||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
|
||||
var replyWidgets = replies.map((e) {
|
||||
var fromMe = e.getMetadata().senderHandle == profile.onion;
|
||||
|
||||
var bubble = StaticMessageBubble(profile, settings, e.getMetadata(), Row(children: [Flexible(child: e.getPreviewWidget(context))]));
|
||||
|
||||
String imagePath = e.getMetadata().senderImage!;
|
||||
var sender = profile.contactList.findContact(e.getMetadata().senderHandle);
|
||||
if (sender != null) {
|
||||
imagePath = showImage ? sender.imagePath : sender.defaultImagePath;
|
||||
}
|
||||
|
||||
if (fromMe) {
|
||||
imagePath = profile.imagePath;
|
||||
}
|
||||
|
||||
var image = Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: ProfileImage(
|
||||
imagePath: imagePath,
|
||||
diameter: 48.0,
|
||||
border: borderColor,
|
||||
badgeTextColor: Colors.red,
|
||||
badgeColor: Colors.red,
|
||||
));
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [image, Flexible(child: bubble)],
|
||||
));
|
||||
}).toList();
|
||||
|
||||
var withHeader = replyWidgets;
|
||||
|
||||
var original =
|
||||
StaticMessageBubble(profile, settings, cache.cache[messageID]!.metadata, Row(children: [Flexible(child: compileOverlay(cache.cache[messageID]!).getPreviewWidget(context))]));
|
||||
|
||||
withHeader.insert(0, Padding(padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0), child: Center(child: original)));
|
||||
|
||||
withHeader.insert(
|
||||
1,
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
|
||||
child: Divider(
|
||||
color: settings.theme.mainTextColor,
|
||||
)));
|
||||
|
||||
if (replies.isNotEmpty) {
|
||||
withHeader.insert(2, Padding(padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0), child: Text(replyHeader, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))));
|
||||
} else {
|
||||
withHeader.insert(
|
||||
2, Padding(padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0), child: Center(child: Text(noRepliesText, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)))));
|
||||
}
|
||||
|
||||
return Scrollbar(
|
||||
isAlwaysShown: true,
|
||||
child: SingleChildScrollView(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: withHeader,
|
||||
)))));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ class ProfileImage extends StatefulWidget {
|
|||
required this.badgeTextColor,
|
||||
this.maskOut = false,
|
||||
this.tooltip = "",
|
||||
this.badgeEdit = false});
|
||||
this.badgeEdit = false,
|
||||
this.badgeIcon = null});
|
||||
final double diameter;
|
||||
final String imagePath;
|
||||
final Color border;
|
||||
|
@ -27,6 +28,7 @@ class ProfileImage extends StatefulWidget {
|
|||
final bool maskOut;
|
||||
final bool badgeEdit;
|
||||
final String tooltip;
|
||||
final Widget? badgeIcon;
|
||||
|
||||
@override
|
||||
_ProfileImageState createState() => _ProfileImageState();
|
||||
|
@ -38,7 +40,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,
|
||||
|
@ -81,7 +83,7 @@ class _ProfileImageState extends State<ProfileImage> {
|
|||
padding: const EdgeInsets.all(2.0), //border size
|
||||
child: ClipOval(clipBehavior: Clip.antiAlias, child: widget.tooltip == "" ? image : Tooltip(message: widget.tooltip, child: image))))),
|
||||
Visibility(
|
||||
visible: widget.badgeEdit || widget.badgeCount > 0,
|
||||
visible: widget.badgeIcon != null || widget.badgeEdit || widget.badgeCount > 0,
|
||||
child: Positioned(
|
||||
bottom: 0.0,
|
||||
right: 0.0,
|
||||
|
@ -93,7 +95,7 @@ class _ProfileImageState extends State<ProfileImage> {
|
|||
Icons.edit,
|
||||
color: widget.badgeTextColor,
|
||||
)
|
||||
: Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0)),
|
||||
: (widget.badgeIcon != null ? widget.badgeIcon : Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0))),
|
||||
),
|
||||
)),
|
||||
]));
|
||||
|
|
|
@ -85,9 +85,9 @@ class _ProfileRowState extends State<ProfileRow> {
|
|||
|
||||
void _pushContactList(ProfileInfoState profile, bool isLandscape) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
PageRouteBuilder(
|
||||
settings: RouteSettings(name: "conversations"),
|
||||
builder: (BuildContext buildcontext) {
|
||||
pageBuilder: (c, a1, a2) {
|
||||
return OrientationBuilder(builder: (orientationBuilderContext, orientation) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider<ProfileInfoState>.value(value: profile), ChangeNotifierProvider<ContactListState>.value(value: profile.contactList)],
|
||||
|
@ -98,24 +98,29 @@ class _ProfileRowState extends State<ProfileRow> {
|
|||
});
|
||||
});
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
|
||||
Provider.of<ErrorHandler>(context, listen: false).reset();
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext bcontext) {
|
||||
var profile = Provider.of<FlwtchState>(bcontext).profs.getProfile(onion)!;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ProfileInfoState>.value(
|
||||
value: profile,
|
||||
),
|
||||
],
|
||||
builder: (context, widget) => AddEditProfileView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (bcontext, a1, a2) {
|
||||
var profile = Provider.of<FlwtchState>(bcontext).profs.getProfile(onion)!;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ProfileInfoState>.value(
|
||||
value: profile,
|
||||
),
|
||||
],
|
||||
builder: (context, widget) => AddEditProfileView(),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
|
||||
transitionDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue