Merge branch 'trunk' of git.openprivacy.ca:cwtch.im/cwtch-ui into ipreview

This commit is contained in:
erinn 2021-12-14 13:37:48 -08:00
commit 63fc1fe772
60 changed files with 2519 additions and 1754 deletions

View File

@ -8,7 +8,7 @@ clone:
steps: steps:
- name: clone - name: clone
image: cirrusci/flutter:dev image: cirrusci/flutter:2.5.3
environment: environment:
buildbot_key_b64: buildbot_key_b64:
from_secret: buildbot_key_b64 from_secret: buildbot_key_b64
@ -24,7 +24,7 @@ steps:
- git checkout $DRONE_COMMIT - git checkout $DRONE_COMMIT
- name: fetch - name: fetch
image: cirrusci/flutter:2.5.0-6.0.pre image: cirrusci/flutter:2.5.3
volumes: volumes:
- name: deps - name: deps
path: /root/.pub-cache path: /root/.pub-cache
@ -47,7 +47,7 @@ steps:
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting # #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
- name: build-linux - name: build-linux
image: openpriv/flutter-desktop:linux-fdev2.5rc image: openpriv/flutter-desktop:linux-fstable-2.5.3
volumes: volumes:
- name: deps - name: deps
path: /root/.pub-cache path: /root/.pub-cache
@ -61,7 +61,7 @@ steps:
- rm -r cwtch - rm -r cwtch
- name: test-build-android - name: test-build-android
image: cirrusci/flutter:2.5.0-6.0.pre image: cirrusci/flutter:2.5.3
when: when:
event: pull_request event: pull_request
volumes: volumes:
@ -71,7 +71,7 @@ steps:
- flutter build apk --debug - flutter build apk --debug
- name: build-android - name: build-android
image: cirrusci/flutter:2.5.0-6.0.pre image: cirrusci/flutter:2.5.3
when: when:
event: push event: push
environment: environment:
@ -95,7 +95,7 @@ steps:
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android #- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
- name: widget-tests - name: widget-tests
image: cirrusci/flutter:2.5.0-6.0.pre image: cirrusci/flutter:2.5.3
volumes: volumes:
- name: deps - name: deps
path: /root/.pub-cache path: /root/.pub-cache
@ -174,7 +174,7 @@ clone:
steps: steps:
- name: clone - name: clone
image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
environment: environment:
buildbot_key_b64: buildbot_key_b64:
from_secret: buildbot_key_b64 from_secret: buildbot_key_b64
@ -192,7 +192,7 @@ steps:
- git checkout $Env:DRONE_COMMIT - git checkout $Env:DRONE_COMMIT
- name: fetch - name: fetch
image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
commands: commands:
- powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip" - powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip"
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }" - powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }"
@ -201,7 +201,7 @@ steps:
- .\fetch-libcwtch-go.ps1 - .\fetch-libcwtch-go.ps1
- name: build-windows - name: build-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
commands: commands:
- flutter pub get - flutter pub get
- $Env:version += type .\VERSION - $Env:version += type .\VERSION
@ -257,7 +257,7 @@ steps:
- move *.sha512 deploy\$Env:builddir - move *.sha512 deploy\$Env:builddir
- name: deploy-windows - name: deploy-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
when: when:
event: push event: push
status: [ success ] status: [ success ]

View File

@ -1 +1 @@
2021-11-09-18-25-v1.4.2 2021-12-11-02-00-v1.5.0-9-gaa102bd

View File

@ -1 +1 @@
2021-11-09-23-25-v1.4.2 2021-12-11-07-00-v1.5.0-9-gaa102bd

View File

@ -28,7 +28,7 @@ Cwtch processes the following environment variables:
First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install). First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install).
You will probably want to disable Analytics on the Flutter Tool: `flutter config --no-analytics` You will probably want to disable Analytics on the Flutter Tool: `flutter config --no-analytics`
This project uses the flutter `dev` channel, which you will need to switch to: `flutter channel dev; flutter upgrade`. This project uses the flutter `stable` channel
Once flutter is set up, run `flutter pub get` from this project folder to fetch dependencies. Once flutter is set up, run `flutter pub get` from this project folder to fetch dependencies.
@ -42,17 +42,20 @@ To build a release version and load normal profiles, use `build-release.sh X` in
- set `LD_LIBRARY_PATH="$PWD/linux"` - set `LD_LIBRARY_PATH="$PWD/linux"`
- copy a `tor` binary to `linux/` or run `fetch-tor.sh` to download one - copy a `tor` binary to `linux/` or run `fetch-tor.sh` to download one
- run `flutter config --enable-linux-desktop` if you've never done so before - run `flutter config --enable-linux-desktop` if you've never done so before
- optional: launch cwtch-ui directly by running `flutter run -d linux` - optional: launch cwtch-ui debug build by running `flutter run -d linux`
- to build cwtch-ui, run `flutter build linux` - to build cwtch-ui, run `flutter build linux`
- optional: launch cwtch-ui build with `env LD_LIBRARY_PATH=linux ./build/linux/x64/release/bundle/cwtch` - optional: launch cwtch-ui release build with `env LD_LIBRARY_PATH=linux ./build/linux/x64/release/bundle/cwtch`
- to package the build, run `linux/package-release.sh` - to package the build, run `linux/package-release.sh`
### Building on Windows (for Windows) ### Building on Windows (for Windows)
- copy `libCwtch.dll` to `windows/`, or run `fetch-libcwtch-go.ps1` to download it - copy `libCwtch.dll` to `windows/`, or run `fetch-libcwtch-go.ps1` to download it
- run `fetch-tor-win.ps1` to fetch Tor for windows - run `fetch-tor-win.ps1` to fetch Tor for windows
- optional: launch cwtch-ui directly by running `flutter run -d windows` - optional: launch cwtch-ui debug build by running `flutter run -d windows`
- to build cwtch-ui, run `flutter build windows` - to build cwtch-ui, run `flutter build windows`
- optional: to run the release build:
- `cp windows/libCwtch.dll .`
- `./build/windows/runner/Release/cwtch.exe`
### Building on Linux/Windows (for Android) ### Building on Linux/Windows (for Android)
@ -65,10 +68,8 @@ To build a release version and load normal profiles, use `build-release.sh X` in
- copy `libCwtch.dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it - copy `libCwtch.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/` - 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` - `flutter build macos`
- optional: launch cwtch-ui build with `./build/linux/x64/release/bundle/cwtch` - optional: launch cwtch-ui release build with `./build/macos/Build/Products/Release/Cwtch.app/Contents/MacOS/Cwtch`
- `./macos/package-release.sh` - To package the UI: `./macos/package-release.sh`, which results in a Cwtch.dmg that has libCwtch.dylib and tor in it as well and can be installed into Applications
results in a Cwtch.dmg that has libCwtch.dylib and tor in it as well and can be installed into Applications
### Known Platform Issues ### Known Platform Issues

View File

@ -48,4 +48,11 @@
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued--> <!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" /> <uses-permission android:name="android.permission.GET_TASKS" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest> </manifest>

View File

@ -133,10 +133,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
notificationManager.notify(dlID, newNotification); notificationManager.notify(dlID, newNotification);
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace()); Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
} }
} else if (evt.EventType == "FileDownloaded") { } else if (evt.EventType == "FileDownloaded") {
Log.i("FlwtchWorker", "file downloaded!"); Log.d("FlwtchWorker", "file downloaded!");
val data = JSONObject(evt.Data); val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile"); val tempFile = data.getString("TempFile");
val fileKey = data.getString("FileKey"); val fileKey = data.getString("FileKey");
@ -147,7 +147,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val targetUri = Uri.parse(filePath); val targetUri = Uri.parse(filePath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri); val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os); val bytesWritten = Files.copy(sourcePath, os);
Log.i("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes"); Log.d("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) { if (bytesWritten != 0L) {
os?.flush(); os?.flush();
os?.close(); os?.close();
@ -181,61 +181,70 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
Cwtch.loadProfiles(pass) Cwtch.loadProfiles(pass)
} }
"GetMessage" -> { "GetMessage" -> {
val profile = (a.get("profile") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val indexI = a.getInt("index") val indexI = a.getInt("index").toLong()
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) 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" -> { "GetMessageByContentHash" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val contentHash = (a.get("contentHash") as? String) ?: ""
return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, handle, contentHash)).build())
}
"UpdateMessageFlags" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val midx = (a.get("midx") as? Long) ?: 0
val flags = (a.get("flags") as? Long) ?: 0
Cwtch.updateMessageFlags(profile, handle, midx, flags)
}
"AcceptContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
Cwtch.acceptContact(profile, handle) 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" -> { "BlockContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
Cwtch.blockContact(profile, handle) Cwtch.blockContact(profile, conversation)
} }
"SendMessage" -> { "SendMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val message = (a.get("message") as? String) ?: "" val message = (a.get("message") as? String) ?: ""
Cwtch.sendMessage(profile, handle, message) Cwtch.sendMessage(profile, conversation, message)
} }
"SendInvitation" -> { "SendInvitation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val target = (a.get("target") as? String) ?: "" val target = (a.get("target") as? Long) ?: -1
Cwtch.sendInvitation(profile, handle, target) Cwtch.sendInvitation(profile, conversation, target)
} }
"ShareFile" -> { "ShareFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: "" val filepath = (a.get("filepath") as? String) ?: ""
Cwtch.shareFile(profile, handle, filepath) Cwtch.shareFile(profile, conversation, filepath)
} }
"DownloadFile" -> { "DownloadFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: "" val filepath = (a.get("filepath") as? String) ?: ""
val manifestpath = (a.get("manifestpath") as? String) ?: "" val manifestpath = (a.get("manifestpath") as? String) ?: ""
val filekey = (a.get("filekey") as? String) ?: "" val filekey = (a.get("filekey") as? String) ?: ""
// FIXME: Prevent spurious calls by Intent // FIXME: Prevent spurious calls by Intent
if (profile != "") { if (profile != "") {
Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey) Cwtch.downloadFile(profile, conversation, filepath, manifestpath, filekey)
} }
} }
"CheckDownloadStatus" -> { "CheckDownloadStatus" -> {
@ -245,9 +254,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
} }
"VerifyOrResumeDownload" -> { "VerifyOrResumeDownload" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = a.getInt("conversation").toLong()
val fileKey = (a.get("fileKey") as? String) ?: "" val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.verifyOrResumeDownload(profile, handle, fileKey) Cwtch.verifyOrResumeDownload(profile, conversation, fileKey)
} }
"SendProfileEvent" -> { "SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: "" val onion = (a.get("onion") as? String) ?: ""
@ -266,13 +275,6 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val bundle = (a.get("bundle") as? String) ?: "" val bundle = (a.get("bundle") as? String) ?: ""
Cwtch.importBundle(profile, bundle) Cwtch.importBundle(profile, bundle)
} }
"SetGroupAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setGroupAttribute(profile, groupHandle, key, value)
}
"CreateGroup" -> { "CreateGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val server = (a.get("server") as? String) ?: "" val server = (a.get("server") as? String) ?: ""
@ -286,18 +288,13 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
} }
"ArchiveConversation" -> { "ArchiveConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val contactHandle = (a.get("handle") as? String) ?: "" val conversation = (a.get("conversation") as? Long) ?: -1
Cwtch.archiveConversation(profile, contactHandle) Cwtch.archiveConversation(profile, conversation)
} }
"DeleteContact" -> { "DeleteConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: "" val conversation = (a.get("conversation") as? Long) ?: -1
Cwtch.deleteContact(profile, handle) Cwtch.deleteContact(profile, conversation)
}
"RejectInvite" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.rejectInvite(profile, groupHandle)
} }
"SetProfileAttribute" -> { "SetProfileAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
@ -305,12 +302,12 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val v = (a.get("Val") as? String) ?: "" val v = (a.get("Val") as? String) ?: ""
Cwtch.setProfileAttribute(profile, key, v) Cwtch.setProfileAttribute(profile, key, v)
} }
"SetContactAttribute" -> { "SetConversationAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val contact = (a.get("Contact") as? String) ?: "" val conversation = (a.get("conversation") as? Long) ?: -1
val key = (a.get("Key") as? String) ?: "" val key = (a.get("Key") as? String) ?: ""
val v = (a.get("Val") as? String) ?: "" val v = (a.get("Val") as? String) ?: ""
Cwtch.setContactAttribute(profile, contact, key, v) Cwtch.setConversationAttribute(profile, conversation, key, v)
} }
"Shutdown" -> { "Shutdown" -> {
Cwtch.shutdownCwtch(); Cwtch.shutdownCwtch();
@ -354,7 +351,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val v = (a.get("Val") as? String) ?: "" val v = (a.get("Val") as? String) ?: ""
Cwtch.setServerAttribute(serverOnion, key, v) Cwtch.setServerAttribute(serverOnion, key, v)
} }
else -> return Result.failure() else -> {
Log.i("FlwtchWorker", "unknown command: " + method);
return Result.failure()
}
} }
return Result.success() return Result.success()
} }

View File

@ -147,11 +147,7 @@ class MainActivity: FlutterActivity() {
// that we can divert this method call to ReconnectCwtchForeground instead if so. // that we can divert this method call to ReconnectCwtchForeground instead if so.
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get() val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
for (workInfo in works) { for (workInfo in works) {
Log.i("handleCwtch:WorkManager", "$workInfo") WorkManager.getInstance(this).cancelWorkById(workInfo.id)
if (!workInfo.tags.contains(uniqueTag)) {
Log.i("handleCwtch:WorkManager", "canceling ${workInfo.id} bc tags don't include $uniqueTag")
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
} }
WorkManager.getInstance(this).pruneWork() WorkManager.getInstance(this).pruneWork()

View File

@ -10,8 +10,6 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground(); Future<void> ReconnectCwtchForeground();
// ignore: non_constant_identifier_names
void SelectProfile(String onion);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass); void CreateProfile(String nick, String pass);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
@ -29,36 +27,39 @@ abstract class Cwtch {
void SendAppEvent(String jsonEvent); void SendAppEvent(String jsonEvent);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle); void AcceptContact(String profileOnion, int contactHandle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle); void BlockContact(String profileOnion, int contactHandle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index); Future<dynamic> GetMessage(String profile, int handle, int index);
// ignore: non_constant_identifier_names
Future<dynamic> GetMessageByContentHash(String profile, String handle, String contentHash);
// ignore: non_constant_identifier_names
void UpdateMessageFlags(String profile, String handle, int index, int flags);
// ignore: non_constant_identifier_names
void SendMessage(String profile, String handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, String handle, String target);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profile, String handle, String filepath); Future<dynamic> GetMessageByID(String profile, int handle, int index);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey); Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey); void SendMessage(String profile, int handle, String message);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, int handle, int target);
// ignore: non_constant_identifier_names
void ShareFile(String profile, int handle, String filepath);
// ignore: non_constant_identifier_names
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
// ignore: non_constant_identifier_names
void CreateDownloadableFile(String profile, int handle, String filenameSuggestion, String filekey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CheckDownloadStatus(String profile, String fileKey); void CheckDownloadStatus(String profile, String fileKey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profile, String handle, String filekey); void VerifyOrResumeDownload(String profile, int handle, String filekey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ArchiveConversation(String profile, String handle); void ArchiveConversation(String profile, int handle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DeleteContact(String profile, String handle); void DeleteContact(String profile, int handle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateGroup(String profile, String server, String groupName); void CreateGroup(String profile, String server, String groupName);
@ -66,14 +67,11 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ImportBundle(String profile, String bundle); void ImportBundle(String profile, String bundle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetGroupAttribute(String profile, String groupHandle, String key, String value);
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle);
// ignore: non_constant_identifier_names
void SetProfileAttribute(String profile, String key, String val); void SetProfileAttribute(String profile, String key, String val);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val); void SetConversationAttribute(String profile, int conversation, String key, String val);
// ignore: non_constant_identifier_names
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void LoadServers(String password); void LoadServers(String password);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/main.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart'; import 'package:cwtch/models/servers.dart';
@ -23,7 +24,8 @@ class CwtchNotifier {
late AppState appState; late AppState appState;
late ServerListState serverListState; late ServerListState serverListState;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) { CwtchNotifier(
ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) {
profileCN = pcn; profileCN = pcn;
settings = settingsCN; settings = settingsCN;
error = errorCN; error = errorCN;
@ -34,6 +36,7 @@ class CwtchNotifier {
} }
void handleMessage(String type, dynamic data) { void handleMessage(String type, dynamic data) {
//EnvironmentConfig.debugLog("NewEvent $type $data");
switch (type) { switch (type) {
case "CwtchStarted": case "CwtchStarted":
appState.SetCwtchInit(); appState.SetCwtchInit();
@ -42,12 +45,15 @@ class CwtchNotifier {
appState.SetAppError(data["Error"]); appState.SetAppError(data["Error"]);
break; break;
case "NewPeer": case "NewPeer":
EnvironmentConfig.debugLog("NewPeer $data");
// if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta... // if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta...
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword"); profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword");
break; break;
case "PeerCreated": case "ContactCreated":
EnvironmentConfig.debugLog("NewServer $data");
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState( profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
data["ProfileOnion"], data["ProfileOnion"],
int.parse(data["ConversationID"]),
data["RemotePeer"], data["RemotePeer"],
nickname: data["nick"], nickname: data["nick"],
status: data["status"], status: data["status"],
@ -64,13 +70,7 @@ class CwtchNotifier {
break; break;
case "NewServer": case "NewServer":
EnvironmentConfig.debugLog("NewServer $data"); EnvironmentConfig.debugLog("NewServer $data");
serverListState.add( serverListState.add(data["Onion"], data["ServerBundle"], data["Running"] == "true", data["Description"], data["Autostart"] == "true", data["StorageType"] == "storage-password");
data["Onion"],
data["ServerBundle"],
data["Running"] == "true",
data["Description"],
data["Autostart"] == "true",
data["StorageType"] == "storage-password");
break; break;
case "ServerIntentUpdate": case "ServerIntentUpdate":
EnvironmentConfig.debugLog("ServerIntentUpdate $data"); EnvironmentConfig.debugLog("ServerIntentUpdate $data");
@ -80,7 +80,6 @@ class CwtchNotifier {
} }
break; break;
case "GroupCreated": case "GroupCreated":
// Retrieve Server Status from Cache... // Retrieve Server Status from Cache...
String status = ""; String status = "";
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]); RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
@ -88,7 +87,7 @@ class CwtchNotifier {
status = serverInfoState.status; status = serverInfoState.status;
} }
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["GroupID"], profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["ConversationID"], data["GroupID"],
authorization: ContactAuthorization.approved, authorization: ContactAuthorization.approved,
imagePath: data["PicturePath"], imagePath: data["PicturePath"],
nickname: data["GroupName"], nickname: data["GroupName"],
@ -111,13 +110,13 @@ class CwtchNotifier {
} }
break; break;
case "DeleteContact": case "DeleteContact":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]);
break; break;
case "DeleteGroup": case "DeleteGroup":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]);
break; break;
case "PeerStateChange": case "PeerStateChange":
ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]); ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
if (contact != null) { if (contact != null) {
if (data["ConnectionState"] != null) { if (data["ConnectionState"] != null) {
contact.status = data["ConnectionState"]; contact.status = data["ConnectionState"];
@ -131,19 +130,29 @@ class CwtchNotifier {
break; break;
case "NewMessageFromPeer": case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!"); notificationManager.notify("New Message From Peer!");
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) { var identifier = int.parse(data["ConversationID"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++; var messageID = int.parse(data["Index"]);
} else { var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.newMarker++; var senderHandle = data['RemotePeer'];
} var senderImage = data['Picture'];
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
// We only ever see messages from authenticated peers. // We might not have received a contact created for this contact yet...
// If the contact is marked as offline then override this - can happen when the contact is removed from the front // In that case the **next** event we receive will actually update these values...
// end during syncing. if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier) != null) {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.isOnline() == false) { if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.status = "Authenticated"; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
} else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now());
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"]);
// We only ever see messages from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated";
}
} }
break; break;
@ -151,43 +160,49 @@ class CwtchNotifier {
// We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end... // We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end...
break; break;
case "IndexedAcknowledgement": case "IndexedAcknowledgement":
var idx = data["Index"]; var conversation = int.parse(data["ConversationID"]);
var messageID = int.parse(data["Index"]);
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation);
// We return -1 for protocol message acks if there is no message // We return -1 for protocol message acks if there is no message
if (idx == "-1") break; if (messageID == -1) break;
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); var key = contact!.getMessageKeyOrFail(conversation, messageID);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break; message.ackd = true;
// We only ever see acks from authenticated peers. // We only ever see acks from authenticated peers.
// If the contact is marked as offline then override this - can happen when the contact is removed from the front // If the contact is marked as offline then override this - can happen when the contact is removed from the front
// end during syncing. // end during syncing.
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.isOnline() == false) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.status = "Authenticated"; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated";
} }
message.ackd = true; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.ackCache(messageID);
} catch (e) { } catch (e) {
// ignore, we received an ack for a message that hasn't loaded onto the screen yet... // ignore, most likely cause is the key got optimized out...
// the protocol was faster than the ui....yay?
} }
break; break;
case "NewMessageFromGroup": case "NewMessageFromGroup":
var identifier = int.parse(data["ConversationID"]);
if (data["ProfileOnion"] != data["RemotePeer"]) { if (data["ProfileOnion"] != data["RemotePeer"]) {
var idx = int.parse(data["Index"]); var idx = int.parse(data["Index"]);
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages; var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages;
// Only bother to do anything if we know about the group and the provided index is greater than our current total... // Only bother to do anything if we know about the group and the provided index is greater than our current total...
if (currentTotal != null && idx >= currentTotal) { if (currentTotal != null && idx >= currentTotal) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages = idx + 1; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"]);
//if not currently open //if not currently open
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) { if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++;
} else { } else {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.newMarker++; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++;
} }
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
// TODO: There are 2 timestamps associated with a new group message - time sent and time received. // TODO: There are 2 timestamps associated with a new group message - time sent and time received.
// Sent refers to the time a profile alleges they sent a message // Sent refers to the time a profile alleges they sent a message
// Received refers to the time we actually saw the message from the server // Received refers to the time we actually saw the message from the server
@ -198,56 +213,24 @@ class CwtchNotifier {
// For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts // For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time` // 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`. // and `local now`.
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], timestampSent.toLocal()); profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, timestampSent.toLocal());
notificationManager.notify("New Message From Group!"); notificationManager.notify("New Message From Group!");
} }
} else { } else {
// from me (already displayed - do not update counter) // This is dealt with by IndexedAcknowledgment
var idx = data["Signature"]; EnvironmentConfig.debugLog("new message from group from yourself - this should not happen");
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])?.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.ackd = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
}
break;
case "MessageCounterResync":
var contactHandle = data["RemotePeer"];
if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"];
var total = int.parse(data["Data"]);
if (total != profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages) {
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = total;
} }
break; break;
case "SendMessageToPeerError": case "SendMessageToPeerError":
// Ignore // Ignore
break; break;
case "IndexedFailure": case "IndexedFailure":
var idx = data["Index"]; var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx); var idx = int.parse(data["Index"]);
try { var key = contact?.getMessageKeyOrFail(contact.identifier, idx);
var message = Provider.of<MessageMetadata>(key!.currentContext!, listen: false); if (key != null) {
message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
break;
case "SendMessageToGroupError":
// from me (already displayed - do not update counter)
EnvironmentConfig.debugLog("SendMessageToGroupError");
var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx);
if (key == null) break;
try {
var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false); var message = Provider.of<MessageMetadata>(key.currentContext!, listen: false);
if (message == null) break;
message.error = true; message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
} }
break; break;
case "AppError": case "AppError":
@ -262,8 +245,8 @@ class CwtchNotifier {
case "UpdateGlobalSettings": case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"])); settings.handleUpdate(jsonDecode(data["Data"]));
break; break;
case "SetAttribute": case "UpdatedProfileAttribute":
if (data["Key"] == "public.profile.name") {//"public.name") { if (data["Key"] == "public.profile.name") {
profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"]; profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"];
} else { } else {
EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}"); EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}");
@ -285,7 +268,6 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]); profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]);
break; break;
case "NewGroup": case "NewGroup":
EnvironmentConfig.debugLog("new group");
String invite = data["GroupInvite"].toString(); String invite = data["GroupInvite"].toString();
if (invite.startsWith("torv3")) { if (invite.startsWith("torv3")) {
String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5))); String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
@ -298,8 +280,9 @@ class CwtchNotifier {
status = serverInfoState.status; status = serverInfoState.status;
} }
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"], var identifier = int.parse(data["ConversationID"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
authorization: ContactAuthorization.approved, authorization: ContactAuthorization.approved,
imagePath: data["PicturePath"], imagePath: data["PicturePath"],
nickname: groupInvite["GroupName"], nickname: groupInvite["GroupName"],
@ -307,7 +290,7 @@ class CwtchNotifier {
status: status, status: status,
isGroup: true, isGroup: true,
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0))); lastMessageTime: DateTime.fromMillisecondsSinceEpoch(0)));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(groupInvite["GroupID"], DateTime.fromMillisecondsSinceEpoch(0)); profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.fromMillisecondsSinceEpoch(0));
} }
} }
break; break;
@ -319,7 +302,6 @@ class CwtchNotifier {
break; break;
case "ServerStateChange": case "ServerStateChange":
// Update the Server Cache // Update the Server Cache
EnvironmentConfig.debugLog("server state changes $data");
profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]); profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) { profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) {
if (contact.isGroup == true && contact.server == data["GroupServer"]) { if (contact.isGroup == true && contact.server == data["GroupServer"]) {
@ -357,13 +339,17 @@ class CwtchNotifier {
} }
break; break;
case "NewRetValMessageFromPeer": case "NewRetValMessageFromPeer":
if (data["Path"] == "name" && data["Data"].toString().trim().length > 0) { if (data["Path"] == "profile.name") {
// Update locally on the UI... if (data["Data"].toString().trim().length > 0) {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) { // Update locally on the UI...
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.nickname = data["Data"]; if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
}
} }
} else if (data['Path'] == "profile.picture") {
// Not yet..
} else { } else {
EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}"); EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
} }
break; break;
case "ManifestSizeReceived": case "ManifestSizeReceived":
@ -385,6 +371,8 @@ class CwtchNotifier {
case "FileDownloaded": case "FileDownloaded":
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]); profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]);
break; break;
case "ImportingProfileEvent":
break;
default: default:
EnvironmentConfig.debugLog("unhandled event: $type"); EnvironmentConfig.debugLog("unhandled event: $type");
} }

View File

@ -36,8 +36,9 @@ typedef VoidFromStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer
typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int); typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); // DownloadFile
typedef VoidFromStringStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int); typedef void_from_string_int_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringStringStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64); typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64);
typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int); typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
@ -51,6 +52,9 @@ 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 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 StringStringFn = void 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 get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len); typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len);
@ -58,10 +62,34 @@ typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len)
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32); 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 GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, 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_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function(
Pointer<Utf8>,
int,
int,
Pointer<Utf8>,
int,
);
// func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char // func c_GetMessagesByContentHash(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, contenthash_ptr *C.char, contenthash_len C.int) *C.char
typedef get_json_blob_from_str_str_str_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); typedef get_json_blob_from_str_str_str_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int); typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int);
typedef void_from_string_int_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntStringStringFn = void Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_int_int_string_string_function = Void Function(Pointer<Utf8>, Int32, Int32, Int32, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringIntIntIntStringStringFn = void Function(Pointer<Utf8>, int, int, int, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef VoidFromStringIntIntFn = void Function(Pointer<Utf8>, int, int, int);
typedef appbus_events_function = Pointer<Utf8> Function(); typedef appbus_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function(); typedef AppbusEventsFn = Pointer<Utf8> Function();
@ -233,16 +261,6 @@ class CwtchFfi implements Cwtch {
} }
} }
// ignore: non_constant_identifier_names
void SelectProfile(String onion) async {
var selectProfileC = library.lookup<NativeFunction<get_json_blob_string_function>>("c_SelectProfile");
// ignore: non_constant_identifier_names
final SelectProfile = selectProfileC.asFunction<GetJsonBlobStringFn>();
final ut8Onion = onion.toNativeUtf8();
SelectProfile(ut8Onion, ut8Onion.length);
malloc.free(ut8Onion);
}
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass) { void CreateProfile(String nick, String pass) {
var createProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_CreateProfile"); var createProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_CreateProfile");
@ -266,17 +284,15 @@ class CwtchFfi implements Cwtch {
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index) async { Future<String> GetMessage(String profile, int handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>("c_GetMessage"); var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessage");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrStrIntFn>(); final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrIntIntFn>();
final utf8profile = profile.toNativeUtf8(); final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8(); Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index);
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index);
String jsonMessage = jsonMessageBytes.toDartString(); String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile); malloc.free(utf8profile);
malloc.free(utf8handle);
return jsonMessage; return jsonMessage;
} }
@ -306,89 +322,75 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle) { void AcceptContact(String profileOnion, int contactHandle) {
var acceptContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_AcceptContact"); var acceptContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_AcceptConversation");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final AcceptContact = acceptContact.asFunction<VoidFromStringStringFn>(); final AcceptContact = acceptContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8(); AcceptContact(u1, u1.length, contactHandle);
AcceptContact(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle) { void BlockContact(String profileOnion, int contactHandle) {
var blockContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_BlockContact"); var blockContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_BlockContact");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final BlockContact = blockContact.asFunction<VoidFromStringStringFn>(); final BlockContact = blockContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8(); BlockContact(u1, u1.length, contactHandle);
BlockContact(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) { void SendMessage(String profileOnion, int contactHandle, String message) {
var sendMessage = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SendMessage"); var sendMessage = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_SendMessage");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SendMessage = sendMessage.asFunction<VoidFromStringStringStringFn>(); final SendMessage = sendMessage.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = message.toNativeUtf8(); final u3 = message.toNativeUtf8();
SendMessage(u1, u1.length, u2, u2.length, u3, u3.length); SendMessage(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, String contactHandle, String target) { void SendInvitation(String profileOnion, int contactHandle, int target) {
var sendInvitation = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SendInvitation"); var sendInvitation = library.lookup<NativeFunction<void_from_string_int_int_function>>("c_SendInvitation");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SendInvitation = sendInvitation.asFunction<VoidFromStringStringStringFn>(); final SendInvitation = sendInvitation.asFunction<VoidFromStringIntIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8(); SendInvitation(u1, u1.length, contactHandle, target);
final u3 = target.toNativeUtf8();
SendInvitation(u1, u1.length, u2, u2.length, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profileOnion, String contactHandle, String filepath) { void ShareFile(String profileOnion, int contactHandle, String filepath) {
var shareFile = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_ShareFile"); var shareFile = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_ShareFile");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final ShareFile = shareFile.asFunction<VoidFromStringStringStringFn>(); final ShareFile = shareFile.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filepath.toNativeUtf8(); final u3 = filepath.toNativeUtf8();
ShareFile(u1, u1.length, u2, u2.length, u3, u3.length); ShareFile(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) { void DownloadFile(String profileOnion, int contactHandle, String filepath, String manifestpath, String filekey) {
var dlFile = library.lookup<NativeFunction<void_from_string_string_string_string_string_function>>("c_DownloadFile"); var dlFile = library.lookup<NativeFunction<void_from_string_int_string_string_string_function>>("c_DownloadFile");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final DownloadFile = dlFile.asFunction<VoidFromStringStringStringStringStringFn>(); final DownloadFile = dlFile.asFunction<VoidFromStringIntStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filepath.toNativeUtf8(); final u3 = filepath.toNativeUtf8();
final u4 = manifestpath.toNativeUtf8(); final u4 = manifestpath.toNativeUtf8();
final u5 = filekey.toNativeUtf8(); final u5 = filekey.toNativeUtf8();
DownloadFile(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length, u5, u5.length); DownloadFile(u1, u1.length, contactHandle, u3, u3.length, u4, u4.length, u5, u5.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
malloc.free(u4); malloc.free(u4);
malloc.free(u5); malloc.free(u5);
@ -396,7 +398,7 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) { void CreateDownloadableFile(String profileOnion, int contactHandle, String filenameSuggestion, String filekey) {
// android only - do nothing // android only - do nothing
} }
@ -415,20 +417,17 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { void VerifyOrResumeDownload(String profileOnion, int contactHandle, String filekey) {
var fn = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_VerifyOrResumeDownload"); var fn = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_VerifyOrResumeDownload");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final VerifyOrResumeDownload = fn.asFunction<VoidFromStringStringStringFn>(); final VerifyOrResumeDownload = fn.asFunction<VoidFromStringIntStringFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
final u3 = filekey.toNativeUtf8(); final u3 = filekey.toNativeUtf8();
VerifyOrResumeDownload(u1, u1.length, u2, u2.length, u3, u3.length); VerifyOrResumeDownload(u1, u1.length, contactHandle, u3, u3.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ResetTor() { void ResetTor() {
@ -451,36 +450,6 @@ class CwtchFfi implements Cwtch {
malloc.free(u2); malloc.free(u2);
} }
@override
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) {
var setGroupAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetGroupAttribute");
// ignore: non_constant_identifier_names
final SetGroupAttribute = setGroupAttribute.asFunction<VoidFromStringStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = value.toNativeUtf8();
SetGroupAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u2);
malloc.free(u3);
malloc.free(u4);
}
@override
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) {
var rejectInvite = library.lookup<NativeFunction<string_string_to_void_function>>("c_RejectInvite");
// ignore: non_constant_identifier_names
final RejectInvite = rejectInvite.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length);
malloc.free(u1);
malloc.free(u2);
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateGroup(String profileOnion, String server, String groupName) { void CreateGroup(String profileOnion, String server, String groupName) {
@ -499,41 +468,24 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ArchiveConversation(String profileOnion, String handle) { void ArchiveConversation(String profileOnion, int handle) {
var archiveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_ArchiveConversation"); var archiveConversation = library.lookup<NativeFunction<string_int_to_void_function>>("c_ArchiveConversation");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final ArchiveConversation = archiveConversation.asFunction<VoidFromStringStringFn>(); final ArchiveConversation = archiveConversation.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8(); ArchiveConversation(u1, u1.length, handle);
ArchiveConversation(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DeleteContact(String profileOnion, String handle) { void DeleteContact(String profileOnion, int handle) {
var deleteContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteContact"); var deleteContact = library.lookup<NativeFunction<string_int_to_void_function>>("c_DeleteContact");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final DeleteContact = deleteContact.asFunction<VoidFromStringStringFn>(); final DeleteContact = deleteContact.asFunction<VoidFromStringIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8(); DeleteContact(u1, u1.length, handle);
DeleteContact(u1, u1.length, u2, u2.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
}
@override
// ignore: non_constant_identifier_names
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
var updateMessageFlagsC = library.lookup<NativeFunction<void_from_string_string_int_int_function>>("c_UpdateMessageFlags");
// ignore: non_constant_identifier_names
final updateMessageFlags = updateMessageFlagsC.asFunction<VoidFromStringStringIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags);
malloc.free(utf8profile);
malloc.free(utf8handle);
} }
@override @override
@ -566,17 +518,30 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val) { void SetConversationAttribute(String profile, int contact, String key, String val) {
var setContactAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetContactAttribute"); var setContactAttribute = library.lookup<NativeFunction<void_from_string_int_string_string_function>>("c_SetConversationAttribute");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringStringStringStringFn>(); final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringIntStringStringFn>();
final u1 = profile.toNativeUtf8(); final u1 = profile.toNativeUtf8();
final u2 = contact.toNativeUtf8();
final u3 = key.toNativeUtf8(); final u3 = key.toNativeUtf8();
final u4 = key.toNativeUtf8(); final u4 = val.toNativeUtf8();
SetContactAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length); SetContactAttribute(u1, u1.length, contact, u3, u3.length, u4, u4.length);
malloc.free(u1);
malloc.free(u3);
malloc.free(u4);
}
@override
// ignore: non_constant_identifier_names
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) {
var setMessageAttribute = library.lookup<NativeFunction<void_from_string_int_int_int_string_string_function>>("c_SetMessageAttribute");
// ignore: non_constant_identifier_names
final SetMessageAttribute = setMessageAttribute.asFunction<VoidFromStringIntIntIntStringStringFn>();
final u1 = profile.toNativeUtf8();
final u3 = key.toNativeUtf8();
final u4 = val.toNativeUtf8();
SetMessageAttribute(u1, u1.length, conversation, channel, message, u3, u3.length, u4, u4.length);
malloc.free(u1); malloc.free(u1);
malloc.free(u2);
malloc.free(u3); malloc.free(u3);
malloc.free(u4); malloc.free(u4);
} }
@ -703,19 +668,17 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future GetMessageByContentHash(String profile, String handle, String contentHash) async { Future GetMessageByContentHash(String profile, int handle, String contentHash) async {
var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_str_str_function>>("c_GetMessagesByContentHash"); var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_int_string_function>>("c_GetMessagesByContentHash");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrStrStrFn>(); final GetMessagesByContentHash = getMessagesByContentHashC.asFunction<GetJsonBlobFromStrIntStringFn>();
final utf8profile = profile.toNativeUtf8(); final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
final utf8contentHash = contentHash.toNativeUtf8(); final utf8contentHash = contentHash.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length); Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, handle, utf8contentHash, utf8contentHash.length);
String jsonMessage = jsonMessageBytes.toDartString(); String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile); malloc.free(utf8profile);
malloc.free(utf8handle);
malloc.free(utf8contentHash); malloc.free(utf8contentHash);
return jsonMessage; return jsonMessage;
} }
@ -734,4 +697,18 @@ class CwtchFfi implements Cwtch {
Map<String, String> envVars = Platform.environment; Map<String, String> envVars = Platform.environment;
return path.join(envVars['HOME']!, "Downloads"); return path.join(envVars['HOME']!, "Downloads");
} }
@override
// ignore: non_constant_identifier_names
Future<String> GetMessageByID(String profile, int handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessageByID");
// ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
} }

View File

@ -68,11 +68,6 @@ class CwtchGomobile implements Cwtch {
cwtchNotifier.handleMessage(call.method, obj); cwtchNotifier.handleMessage(call.method, obj);
} }
// ignore: non_constant_identifier_names
void SelectProfile(String onion) {
cwtchPlatform.invokeMethod("SelectProfile", {"profile": onion});
}
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass) { void CreateProfile(String nick, String pass) {
cwtchPlatform.invokeMethod("CreateProfile", {"nick": nick, "pass": pass}); cwtchPlatform.invokeMethod("CreateProfile", {"nick": nick, "pass": pass});
@ -89,9 +84,13 @@ class CwtchGomobile implements Cwtch {
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index) { Future<dynamic> GetMessage(String profile, int conversation, int index) {
print("gomobile.dart GetMessage " + index.toString()); return cwtchPlatform.invokeMethod("GetMessage", {"ProfileOnion": profile, "conversation": conversation, "index": index});
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index}); }
// ignore: non_constant_identifier_names
Future<dynamic> GetMessageByID(String profile, int conversation, int id) {
return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id});
} }
@override @override
@ -111,43 +110,43 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void AcceptContact(String profileOnion, String contactHandle) { void AcceptContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "conversation": conversation});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle) { void BlockContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "conversation": conversation});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) { void SendMessage(String profileOnion, int conversation, String message) {
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "handle": contactHandle, "message": message}); cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, String contactHandle, String target) { void SendInvitation(String profileOnion, int conversation, int target) {
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target}); cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profileOnion, String contactHandle, String filepath) { void ShareFile(String profileOnion, int conversation, String filepath) {
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath}); cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) { void DownloadFile(String profileOnion, int conversation, String filepath, String manifestpath, String filekey) {
cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey}); cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey});
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) { void CreateDownloadableFile(String profileOnion, int conversation, String filenameSuggestion, String filekey) {
cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey}); cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filename": filenameSuggestion, "filekey": filekey});
} }
@override @override
@ -158,8 +157,8 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { void VerifyOrResumeDownload(String profileOnion, int conversation, String filekey) {
cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "handle": contactHandle, "filekey": filekey}); cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "conversation": conversation, "filekey": filekey});
} }
@override @override
@ -174,18 +173,6 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("ImportBundle", {"ProfileOnion": profileOnion, "bundle": bundle}); cwtchPlatform.invokeMethod("ImportBundle", {"ProfileOnion": profileOnion, "bundle": bundle});
} }
@override
// ignore: non_constant_identifier_names
void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) {
cwtchPlatform.invokeMethod("SetGroupAttribute", {"ProfileOnion": profileOnion, "groupHandle": groupHandle, "key": key, "value": value});
}
@override
// ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
}
@override @override
void CreateGroup(String profileOnion, String server, String groupName) { void CreateGroup(String profileOnion, String server, String groupName) {
cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName}); cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName});
@ -193,20 +180,14 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DeleteContact(String profileOnion, String handle) { void DeleteContact(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "handle": handle}); cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "conversation": conversation});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ArchiveConversation(String profileOnion, String contactHandle) { void ArchiveConversation(String profileOnion, int conversation) {
cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "handle": contactHandle}); cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "conversation": conversation});
}
@override
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
print("gomobile.dart UpdateMessageFlags " + index.toString());
cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "midx": index, "flags": flags});
} }
@override @override
@ -217,8 +198,8 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SetContactAttribute(String profile, String contact, String key, String val) { void SetConversationAttribute(String profile, int conversation, String key, String val) {
cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "Contact": contact, "Key": key, "Val": val}); cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "conversation": conversation, "Key": key, "Val": val});
} }
@override @override
@ -275,15 +256,20 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val}); cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val});
} }
@override @override
Future<void> Shutdown() async { Future<void> Shutdown() async {
print("gomobile.dart Shutdown"); print("gomobile.dart Shutdown");
cwtchPlatform.invokeMethod("Shutdown", {}); cwtchPlatform.invokeMethod("Shutdown", {});
} }
@override @override
Future GetMessageByContentHash(String profile, String handle, String contentHash) { Future GetMessageByContentHash(String profile, int conversation, String contentHash) {
return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "contentHash": contentHash}); return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"ProfileOnion": profile, "conversation": conversation, "contentHash": contentHash});
}
@override
void SetMessageAttribute(String profile, int conversation, int channel, int message, String key, String val) {
cwtchPlatform.invokeMethod("SetMessageAttribute", {"ProfileOnion": profile, "conversation": conversation, "Channel": channel, "Message": message, "Key": key, "Val": val});
} }
@override @override

View File

@ -1,6 +1,20 @@
{ {
"@@locale": "de", "@@locale": "de",
"@@last_modified": "2021-11-11T01:02:08+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"newMessagesLabel": "New Messages", "newMessagesLabel": "New Messages",
"localeRU": "Russian", "localeRU": "Russian",
"copyServerKeys": "Copy keys", "copyServerKeys": "Copy keys",
@ -8,8 +22,8 @@
"fileCheckingStatus": "Checking download status", "fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted", "fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to", "fileSavedTo": "Saved to",
"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.",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.", "encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"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": "Really delete server", "deleteServerConfirmBtn": "Really delete server",
"deleteServerSuccess": "Successfully deleted server", "deleteServerSuccess": "Successfully deleted server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
@ -32,8 +46,8 @@
"serverAddress": "Server Address", "serverAddress": "Server Address",
"editServerTitle": "Edit Server", "editServerTitle": "Edit Server",
"addServerTitle": "Add Server", "addServerTitle": "Add Server",
"downloadFileButton": "Herunterladen",
"titleManageProfilesShort": "Profiles", "titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.", "descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing", "settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File", "tooltipSendFile": "Send File",
@ -42,25 +56,12 @@
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.", "messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
"labelFilesize": "Size", "labelFilesize": "Size",
"labelFilename": "Filename", "labelFilename": "Filename",
"downloadFileButton": "Herunterladen",
"openFolderButton": "Open Folder", "openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...", "retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"streamerModeLabel": "Streamer\/Presentation Mode", "streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation", "archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"addPeerTab": "Einen anderen Nutzer hinzufügen",
"addPeer": "Anderen Nutzer hinzufügen",
"peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.",
"peerBlockedMessage": "Anderer Nutzer ist blockiert",
"peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"blockBtn": "Anderen Nutzer blockieren",
"savePeerHistory": "Peer-Verlauf speichern",
"savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen",
"unblockBtn": "Anderen Nutzer entsperren",
"blockUnknownLabel": "Unbekannte Peers blockieren",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings", "blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"showMessageButton": "Show Message", "showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.", "blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...", "placeholderEnterMessage": "Type a message...",
@ -80,17 +81,31 @@
"tooltipReplyToThisMessage": "Reply to this message", "tooltipReplyToThisMessage": "Reply to this message",
"tooltipRejectContactRequest": "Reject this contact request", "tooltipRejectContactRequest": "Reject this contact request",
"tooltipAcceptContactRequest": "Accept this contact request.", "tooltipAcceptContactRequest": "Accept this contact request.",
"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",
"deleteConfirmLabel": "Gib LÖSCHEN ein um zu bestätigen",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"cycleColoursAndroid": "Klicken um Farbe zu wechseln.\nGedrückt halten zum zurücksetzen.",
"cycleMorphsDesktop": "Klicken um Morph zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleMorphsAndroid": "Klicken um Morph zu wechseln.\nGedrückt halten zum zurücksetzen.",
"pasteAddressToAddContact": "Adresse, Einladung oder Schlüssel hier hinzufügen, um einen Kontakt hinzuzufügen",
"notificationNewMessageFromGroup": "Neue Nachricht in einer Gruppe!", "notificationNewMessageFromGroup": "Neue Nachricht in einer Gruppe!",
"notificationNewMessageFromPeer": "Neue Nachricht von einem Kontakt!", "notificationNewMessageFromPeer": "Neue Nachricht von einem Kontakt!",
"tooltipHidePassword": "Password verstecken", "tooltipHidePassword": "Password verstecken",
"tooltipShowPassword": "Password anzeigen", "tooltipShowPassword": "Password anzeigen",
"serverNotSynced": "Neue Nachrichten abrufen (Dies kann eine Weile dauern...)",
"groupInviteSettingsWarning": "Du wurdest eingeladen einer Gruppe beizutreten! Bitte aktiviere das Gruppenchat Experiment in den Einstellungen um diese Einladung anzusehen.", "groupInviteSettingsWarning": "Du wurdest eingeladen einer Gruppe beizutreten! Bitte aktiviere das Gruppenchat Experiment in den Einstellungen um diese Einladung anzusehen.",
"shutdownCwtchAction": "Cwtch schliessen", "shutdownCwtchAction": "Cwtch schliessen",
"shutdownCwtchDialog": "Bist du sicher, dass du Cwtch schliessen möchtest? Alle Verbindungen werden geschlossen und die App wird beendet.", "shutdownCwtchDialog": "Bist du sicher, dass du Cwtch schliessen möchtest? Alle Verbindungen werden geschlossen und die App wird beendet.",
"shutdownCwtchDialogTitle": "Cwtch schliessen?", "shutdownCwtchDialogTitle": "Cwtch schliessen?",
"shutdownCwtchTooltip": "Cwtch schliessen", "shutdownCwtchTooltip": "Cwtch schliessen",
"malformedMessage": "Fehlerhafte Nachricht",
"profileDeleteSuccess": "Profil erfolgreich gelöscht", "profileDeleteSuccess": "Profil erfolgreich gelöscht",
"debugLog": "Konsolendebuglogging aktivivieren", "debugLog": "Konsolendebuglogging aktivivieren",
"torNetworkStatus": "Tor Netzwerkstatus", "torNetworkStatus": "Tor Netzwerkstatus",
@ -106,7 +121,6 @@
"torStatus": "Tor Status", "torStatus": "Tor Status",
"torVersion": "Tor Version", "torVersion": "Tor Version",
"sendAnInvitation": "Du hast eine Einladung geschickt für: ", "sendAnInvitation": "Du hast eine Einladung geschickt für: ",
"contactSuggestion": "Dieser Kontaktvorschlag ist für: ",
"rejected": "Abgelehnt!", "rejected": "Abgelehnt!",
"accepted": "Angenommen!", "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.", "chatHistoryDefault": "Diese Unterhaltung wird gelöscht sobald Cwtch geschlossen wird! Der Nachrichtenverlauf für jede Unterhaltung kann im Einstellungsmenü oben rechts geändert werden.",
@ -115,9 +129,6 @@
"reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Unterhaltung beenden möchtest? Alle Nachrichten und Attribute werden gelöscht.", "reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Unterhaltung beenden möchtest? Alle Nachrichten und Attribute werden gelöscht.",
"leaveGroup": "Unterhaltung beenden", "leaveGroup": "Unterhaltung beenden",
"inviteToGroup": "Du wurdest eingeladen einer Gruppe beizutreten:", "inviteToGroup": "Du wurdest eingeladen einer Gruppe beizutreten:",
"pasteAddressToAddContact": "Adresse, Einladung oder Schlüssel hier hinzufügen, um einen Kontakt hinzuzufügen",
"tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen",
"titleManageContacts": "Unterhaltungen",
"titleManageServers": "Server verwalten", "titleManageServers": "Server verwalten",
"dateNever": "Nie", "dateNever": "Nie",
"dateLastYear": "Letzes Jahr", "dateLastYear": "Letzes Jahr",
@ -125,81 +136,110 @@
"dateLastMonth": "Letzter Monat", "dateLastMonth": "Letzter Monat",
"dateRightNow": "Jetzt", "dateRightNow": "Jetzt",
"successfullAddedContact": "Erfolgreich hinzugefügt", "successfullAddedContact": "Erfolgreich hinzugefügt",
"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.",
"titleManageProfiles": "Cwtch Profile verwalten", "titleManageProfiles": "Cwtch Profile verwalten",
"tooltipUnlockProfiles": "Entsperre verschlüsselte Profile durch Eingabe des Passworts.", "tooltipUnlockProfiles": "Entsperre verschlüsselte Profile durch Eingabe des Passworts.",
"titleManageContacts": "Unterhaltungen",
"tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen",
"tooltipOpenSettings": "Öfffne das Einstellungsmenü", "tooltipOpenSettings": "Öfffne das Einstellungsmenü",
"invalidImportString": "Ungültiger Importstring",
"contactAlreadyExists": "Kontakt existiert bereits", "contactAlreadyExists": "Kontakt existiert bereits",
"invalidImportString": "Ungültiger Importstring",
"conversationSettings": "Unterhaltungseinstellungen", "conversationSettings": "Unterhaltungseinstellungen",
"enterCurrentPasswordForDelete": "Bitte gib das aktuelle Passwort ein, um diese Profil zu löschen.", "enterCurrentPasswordForDelete": "Bitte gib das aktuelle Passwort ein, um diese Profil zu löschen.",
"enableGroups": "Gruppenchat aktivieren", "enableGroups": "Gruppenchat aktivieren",
"experimentsEnabled": "Experimente aktiviert",
"localeIt": "Italiana", "localeIt": "Italiana",
"localeEs": "Espanol", "localeEs": "Espanol",
"addListItem": "Liste hinzufügen",
"addNewItem": "Ein neues Element zur Liste hinzufügen",
"todoPlaceholder": "noch zu erledigen",
"newConnectionPaneTitle": "Neue Verbindung",
"networkStatusOnline": "Online",
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
"networkStatusDisconnected": "Vom Internet getrennt, überprüfe deine Verbindung",
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
"loadingTor": "Tor wird geladen...",
"smallTextLabel": "Klein",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
"builddate": "Aufgebaut auf: %2",
"version": "Version %1",
"versionTor": "Version %1 mit tor %2",
"themeDark": "Dunkel",
"themeLight": "Licht",
"settingTheme": "Thema",
"largeTextLabel": "Groß",
"settingInterfaceZoom": "Zoomstufe",
"localeDe": "Deutsche",
"localePt": "Portuguesa", "localePt": "Portuguesa",
"localeFr": "Frances", "localeFr": "Frances",
"localeEn": "English", "localeEn": "English",
"settingLanguage": "Sprache",
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"cwtchSettingsTitle": "Cwtch Einstellungen",
"unlock": "Entsperren",
"yourServers": "Deine Server",
"yourProfiles": "Deine Profile",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"password": "Passwort",
"enterProfilePassword": "Gib ein Passwort ein, um deine Profile anzuzeigen",
"addNewProfileBtn": "Neues Profil hinzufügen",
"deleteConfirmText": "LÖSCHEN",
"deleteProfileConfirmBtn": "Profil wirklich löschen",
"deleteConfirmLabel": "Gib LÖSCHEN ein um zu bestätigen",
"deleteProfileBtn": "Profil löschen",
"passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt",
"passwordErrorMatch": "Passwörter stimmen nicht überein",
"saveProfileBtn": "Profil speichern",
"createProfileBtn": "Profil speichern",
"passwordErrorEmpty": "Passwort darf nicht leer sein", "passwordErrorEmpty": "Passwort darf nicht leer sein",
"password2Label": "Passwort erneut eingeben",
"password1Label": "Passwort",
"currentPasswordLabel": "aktuelles Passwort", "currentPasswordLabel": "aktuelles Passwort",
"yourDisplayName": "Dein Anzeigename", "yourDisplayName": "Dein Anzeigename",
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.", "unblockBtn": "Anderen Nutzer entsperren",
"radioNoPassword": "Unverschlüsselt (kein Passwort)", "dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen",
"radioUsePassword": "Passwort", "savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"copiedToClipboardNotification": "in die Zwischenablage kopiert", "blockBtn": "Anderen Nutzer blockieren",
"copyBtn": "Kopieren", "displayNameLabel": "Angezeigename",
"editProfile": "Profil bearbeiten", "peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"newProfile": "Neues Profil", "peerBlockedMessage": "Anderer Nutzer ist blockiert",
"defaultProfileName": "Alice", "dmTooltip": "Klicken, um Direktnachricht zu senden",
"profileName": "Anzeigename", "peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.",
"editProfileTitle": "Profil bearbeiten", "searchList": "Liste durchsuchen",
"update": "Update",
"viewServerInfo": "Serverinfo",
"serverNotSynced": "Neue Nachrichten abrufen (Dies kann eine Weile dauern...)",
"serverSynced": "synchronisiert",
"cycleColoursDesktop": "Klicken um Farbe zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleCatsDesktop": "Klicken um Kategorie zu wechseln.\nRechtslick zum zurücksetzen.",
"cycleCatsAndroid": "Klicken um Kategorie zu wechseln.\nLanger Klick zum zurücksetzen.",
"addPeer": "Anderen Nutzer hinzufügen",
"addPeerTab": "Einen anderen Nutzer hinzufügen",
"todoPlaceholder": "noch zu erledigen",
"addListItem": "Liste hinzufügen",
"addNewItem": "Ein neues Element zur Liste hinzufügen",
"createGroupTab": "Eine Gruppe erstellen",
"joinGroupTab": "Einer Gruppe beitreten",
"peerAddress": "Adresse",
"peerName": "Namen",
"groupName": "Gruppenname",
"server": "Server",
"invitation": "Einladung",
"groupAddr": "Adresse",
"createGroup": "Gruppe erstellen",
"joinGroup": "Gruppe beitreten",
"blocked": "Blockiert",
"search": "Suche...",
"serverInfo": "Server-Informationen",
"serverConnectivityConnected": "Server verbunden",
"serverConnectivityDisconnected": "Server getrennt",
"addListItemBtn": "Element hinzufügen",
"savePeerHistory": "Peer-Verlauf speichern",
"addProfileTitle": "Neues Profil hinzufügen", "addProfileTitle": "Neues Profil hinzufügen",
"editProfileTitle": "Profil bearbeiten",
"profileName": "Anzeigename",
"defaultProfileName": "Alice",
"newProfile": "Neues Profil",
"editProfile": "Profil bearbeiten",
"radioUsePassword": "Passwort",
"radioNoPassword": "Unverschlüsselt (kein Passwort)",
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.",
"deleteProfileBtn": "Profil löschen",
"deleteConfirmText": "LÖSCHEN",
"deleteProfileConfirmBtn": "Profil wirklich löschen",
"addNewProfileBtn": "Neues Profil hinzufügen",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"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",
"password": "Passwort",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"unlock": "Entsperren",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"blockUnknownLabel": "Unbekannte Peers blockieren",
"settingLanguage": "Sprache",
"localeDe": "Deutsche",
"settingInterfaceZoom": "Zoomstufe",
"settingTheme": "Thema",
"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",
"deleteBtn": "Löschen", "deleteBtn": "Löschen",
"saveBtn": "Speichern", "saveBtn": "Speichern",
"displayNameLabel": "Angezeigename",
"addressLabel": "Adresse", "addressLabel": "Adresse",
"puzzleGameBtn": "Puzzlespiel", "puzzleGameBtn": "Puzzlespiel",
"bulletinsBtn": "Meldungen", "bulletinsBtn": "Meldungen",
@ -210,45 +250,19 @@
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen", "acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen",
"newGroupBtn": "Neue Gruppe anlegen", "newGroupBtn": "Neue Gruppe anlegen",
"copiedClipboardNotification": "in die Zwischenablage kopiert", "copiedClipboardNotification": "in die Zwischenablage kopiert",
"copyBtn": "Kopieren",
"pendingLabel": "Bestätigung ausstehend", "pendingLabel": "Bestätigung ausstehend",
"acknowledgedLabel": "bestätigt", "acknowledgedLabel": "bestätigt",
"couldNotSendMsgError": "Nachricht konnte nicht gesendet werden", "couldNotSendMsgError": "Nachricht konnte nicht gesendet werden",
"dmTooltip": "Klicken, um Direktnachricht zu senden",
"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.", "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.",
"addListItemBtn": "Element hinzufügen",
"searchList": "Liste durchsuchen",
"update": "Update",
"inviteBtn": "Einladen", "inviteBtn": "Einladen",
"inviteToGroupLabel": "In die Gruppe einladen", "inviteToGroupLabel": "In die Gruppe einladen",
"groupNameLabel": "Gruppenname", "groupNameLabel": "Gruppenname",
"viewServerInfo": "Serverinfo",
"serverSynced": "synchronisiert",
"serverConnectivityDisconnected": "Server getrennt",
"serverConnectivityConnected": "Server verbunden",
"serverInfo": "Server-Informationen",
"invitationLabel": "Einladung", "invitationLabel": "Einladung",
"serverLabel": "Server", "serverLabel": "Server",
"search": "Suche...",
"cycleColoursDesktop": "Klicken um Farbe zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleColoursAndroid": "Klicken um Farbe zu wechseln.\nGedrückt halten zum zurücksetzen.",
"cycleMorphsDesktop": "Klicken um Morph zu wechseln.\nRechtsklick zum zurücksetzen.",
"cycleMorphsAndroid": "Klicken um Morph zu wechseln.\nGedrückt halten zum zurücksetzen.",
"cycleCatsDesktop": "Klicken um Kategorie zu wechseln.\nRechtslick zum zurücksetzen.",
"cycleCatsAndroid": "Klicken um Kategorie zu wechseln.\nLanger Klick zum zurücksetzen.",
"blocked": "Blockiert",
"titlePlaceholder": "Titel...", "titlePlaceholder": "Titel...",
"postNewBulletinLabel": "Neue Meldung veröffentlichen", "postNewBulletinLabel": "Neue Meldung veröffentlichen",
"newBulletinLabel": "Neue Meldung", "newBulletinLabel": "Neue Meldung",
"joinGroup": "Gruppe beitreten",
"createGroup": "Gruppe erstellen",
"groupAddr": "Adresse",
"invitation": "Einladung",
"server": "Server",
"groupName": "Gruppenname",
"peerName": "Namen",
"peerAddress": "Adresse",
"joinGroupTab": "Einer Gruppe beitreten",
"createGroupTab": "Eine Gruppe erstellen",
"createGroupBtn": "Anlegen", "createGroupBtn": "Anlegen",
"defaultGroupName": "Tolle Gruppe", "defaultGroupName": "Tolle Gruppe",
"createGroupTitle": "Gruppe Anlegen" "createGroupTitle": "Gruppe Anlegen"

View File

@ -1,6 +1,21 @@
{ {
"@@locale": "en", "@@locale": "en",
"@@last_modified": "2021-11-11T01:02:08+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.",
"newMessagesLabel": "New Messages", "newMessagesLabel": "New Messages",
"localeRU": "Russian", "localeRU": "Russian",
"copyServerKeys": "Copy keys", "copyServerKeys": "Copy keys",
@ -55,7 +70,6 @@
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now", "peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
"blockBtn": "Block Contact", "blockBtn": "Block Contact",
"savePeerHistory": "Save History", "savePeerHistory": "Save History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
"dontSavePeerHistory": "Delete History", "dontSavePeerHistory": "Delete History",
"unblockBtn": "Unblock Contact", "unblockBtn": "Unblock Contact",
"blockUnknownLabel": "Block Unknown Contacts", "blockUnknownLabel": "Block Unknown Contacts",
@ -190,7 +204,6 @@
"radioNoPassword": "Unencrypted (No password)", "radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password", "radioUsePassword": "Password",
"copiedToClipboardNotification": "Copied to Clipboard", "copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"editProfile": "Edit Profille", "editProfile": "Edit Profille",
"newProfile": "New Profile", "newProfile": "New Profile",
"defaultProfileName": "Alice", "defaultProfileName": "Alice",
@ -210,6 +223,7 @@
"acceptGroupInviteLabel": "Do you want to accept the invitation to", "acceptGroupInviteLabel": "Do you want to accept the invitation to",
"newGroupBtn": "Create new group", "newGroupBtn": "Create new group",
"copiedClipboardNotification": "Copied to clipboard", "copiedClipboardNotification": "Copied to clipboard",
"copyBtn": "Copy",
"pendingLabel": "Pending", "pendingLabel": "Pending",
"acknowledgedLabel": "Acknowledged", "acknowledgedLabel": "Acknowledged",
"couldNotSendMsgError": "Could not send this message", "couldNotSendMsgError": "Could not send this message",

View File

@ -1,6 +1,20 @@
{ {
"@@locale": "es", "@@locale": "es",
"@@last_modified": "2021-11-11T01:02:08+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"newMessagesLabel": "New Messages", "newMessagesLabel": "New Messages",
"localeRU": "Russian", "localeRU": "Russian",
"copyServerKeys": "Copy keys", "copyServerKeys": "Copy keys",
@ -8,8 +22,8 @@
"fileCheckingStatus": "Checking download status", "fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted", "fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to", "fileSavedTo": "Saved to",
"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.",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.", "encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"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": "Really delete server", "deleteServerConfirmBtn": "Really delete server",
"deleteServerSuccess": "Successfully deleted server", "deleteServerSuccess": "Successfully deleted server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
@ -33,7 +47,6 @@
"editServerTitle": "Edit Server", "editServerTitle": "Edit Server",
"addServerTitle": "Add Server", "addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles", "titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.", "descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing", "settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File", "tooltipSendFile": "Send File",
@ -45,22 +58,10 @@
"downloadFileButton": "Download", "downloadFileButton": "Download",
"openFolderButton": "Open Folder", "openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...", "retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"streamerModeLabel": "Streamer\/Presentation Mode", "streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation", "archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
"addPeerTab": "Agregar Contacto",
"addPeer": "Agregar Contacto",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"peerBlockedMessage": "Contacto bloqueado",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"blockBtn": "Bloquear contacto",
"savePeerHistory": "Guardar el historial con contacto",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"dontSavePeerHistory": "Eliminar historial de contacto",
"unblockBtn": "Desbloquear contacto",
"blockUnknownLabel": "Bloquear conexiones desconocidas",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings", "blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Conectando a la red y a los contactos...",
"showMessageButton": "Show Message", "showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.", "blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...", "placeholderEnterMessage": "Type a message...",
@ -84,7 +85,6 @@
"notificationNewMessageFromPeer": "New message from a contact!", "notificationNewMessageFromPeer": "New message from a contact!",
"tooltipHidePassword": "Hide Password", "tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password", "tooltipShowPassword": "Show Password",
"serverNotSynced": "Fuera de sincronización con el servidor",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch", "shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
@ -115,9 +115,6 @@
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation", "leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:", "inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers", "titleManageServers": "Manage Servers",
"dateNever": "Never", "dateNever": "Never",
"dateLastYear": "Last Year", "dateLastYear": "Last Year",
@ -130,126 +127,143 @@
"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.", "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", "titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"titleManageContacts": "Conversations",
"tooltipAddContact": "Add a new contact or conversation",
"tooltipOpenSettings": "Open the settings pane", "tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists", "contactAlreadyExists": "Contact Already Exists",
"invalidImportString": "Invalid import string",
"conversationSettings": "Conversation Settings", "conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat", "enableGroups": "Enable Group Chat",
"experimentsEnabled": "Experimentos habilitados",
"localeIt": "Italiano",
"localeEs": "Español",
"addListItem": "Añadir un nuevo elemento a la lista",
"addNewItem": "Añadir un nuevo elemento a la lista",
"todoPlaceholder": "Por hacer...",
"newConnectionPaneTitle": "Nueva conexión",
"networkStatusOnline": "En línea",
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor",
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
"viewGroupMembershipTooltip": "Ver membresía del grupo",
"loadingTor": "Cargando tor...",
"smallTextLabel": "Pequeño",
"defaultScalingText": "Tamaño predeterminado de texto (factor de escala:", "defaultScalingText": "Tamaño predeterminado de texto (factor de escala:",
"builddate": "Basado en: %2", "todoPlaceholder": "Por hacer...",
"version": "Versión %1", "bulletinsBtn": "Boletines",
"versionTor": "Versión %1 con tor %2", "radioNoPassword": "Sin cifrado (sin contraseña)",
"themeDark": "Oscuro", "themeDark": "Oscuro",
"themeLight": "Claro", "smallTextLabel": "Pequeño",
"settingTheme": "Tema", "loadingTor": "Cargando tor...",
"largeTextLabel": "Grande", "cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"settingInterfaceZoom": "Nivel de zoom", "cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"builddate": "Basado en: %2",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"localeDe": "Alemán", "localeDe": "Alemán",
"localePt": "Portugués", "localePt": "Portugués",
"localeFr": "Francés", "localeFr": "Francés",
"localeEn": "Inglés", "addListItem": "Añadir un nuevo elemento a la lista",
"settingLanguage": "Idioma", "unblockBtn": "Desbloquear contacto",
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)",
"versionBuilddate": "Versión: %1 Basado en %2",
"cwtchSettingsTitle": "Configuración de Cwtch",
"unlock": "Desbloquear",
"yourServers": "Tus servidores",
"yourProfiles": "Tus perfiles",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"password": "Contraseña",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"addNewProfileBtn": "Agregar nuevo perfil",
"deleteConfirmText": "ELIMINAR",
"deleteProfileConfirmBtn": "Confirmar eliminar perfil",
"deleteConfirmLabel": "Escribe ELIMINAR para confirmar",
"deleteProfileBtn": "Eliminar Perfil",
"passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada",
"passwordErrorMatch": "Las contraseñas no coinciden",
"saveProfileBtn": "Guardar perfil",
"createProfileBtn": "Crear perfil",
"passwordErrorEmpty": "El campo de contraseña no puede estar vacío",
"password2Label": "Vuelve a ingresar tu contraseña",
"password1Label": "Contraseña",
"currentPasswordLabel": "Contraseña actual",
"yourDisplayName": "Tu nombre de usuario",
"noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados",
"radioNoPassword": "Sin cifrado (sin contraseña)",
"radioUsePassword": "Contraseña",
"copiedToClipboardNotification": "Copiado al portapapeles",
"copyBtn": "Copiar",
"editProfile": "Editar perfil",
"newProfile": "Nuevo perfil",
"defaultProfileName": "Alicia",
"profileName": "Nombre de Usuario",
"editProfileTitle": "Editar perfil",
"addProfileTitle": "Agregar nuevo perfil",
"deleteBtn": "Eliminar",
"saveBtn": "Guardar",
"displayNameLabel": "Nombre de Usuario",
"addressLabel": "Dirección",
"puzzleGameBtn": "Juego de rompecabezas",
"bulletinsBtn": "Boletines",
"listsBtn": "Listas",
"chatBtn": "Chat",
"rejectGroupBtn": "Rechazar",
"acceptGroupBtn": "Aceptar",
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
"newGroupBtn": "Crear un nuevo grupo de chat",
"copiedClipboardNotification": "Copiado al portapapeles",
"pendingLabel": "Pendiente",
"acknowledgedLabel": "Reconocido",
"couldNotSendMsgError": "No se pudo enviar este mensaje",
"dmTooltip": "Haz clic para enviar mensaje directo",
"membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo",
"addListItemBtn": "Agregar artículo",
"searchList": "Buscar en la lista",
"update": "Actualizar",
"inviteBtn": "Invitar",
"inviteToGroupLabel": "Invitar al grupo",
"groupNameLabel": "Nombre del grupo",
"viewServerInfo": "Información del servidor",
"serverSynced": "Sincronizado",
"serverConnectivityDisconnected": "Servidor desconectado",
"serverConnectivityConnected": "Servidor conectado",
"serverInfo": "Información del servidor",
"invitationLabel": "Invitación",
"serverLabel": "Servidor",
"search": "Búsqueda...",
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"blocked": "Bloqueado",
"titlePlaceholder": "título...",
"postNewBulletinLabel": "Publicar nuevo boletín",
"newBulletinLabel": "Nuevo Boletín",
"joinGroup": "Únete al grupo",
"createGroup": "Crear perfil",
"groupAddr": "Dirección",
"invitation": "Invitación",
"server": "Servidor",
"groupName": "Nombre del grupo",
"peerName": "Nombre",
"peerAddress": "Dirección",
"joinGroupTab": "Únete a un grupo", "joinGroupTab": "Únete a un grupo",
"createGroupTab": "Crear un grupo", "viewGroupMembershipTooltip": "Ver membresía del grupo",
"createGroupBtn": "Crear", "peerBlockedMessage": "Contacto bloqueado",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
"couldNotSendMsgError": "No se pudo enviar este mensaje",
"pendingLabel": "Pendiente",
"chatBtn": "Chat",
"dontSavePeerHistory": "Eliminar historial de contacto",
"password": "Contraseña",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"networkStatusConnecting": "Conectando a la red y a los contactos...",
"localeIt": "Italiano",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"acknowledgedLabel": "Reconocido",
"blockBtn": "Bloquear contacto",
"savePeerHistory": "Guardar el historial con contacto",
"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)",
"settingTheme": "Tema",
"themeLight": "Claro",
"experimentsEnabled": "Experimentos habilitados",
"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",
"createGroupTitle": "Crear un grupo",
"serverLabel": "Servidor",
"groupNameLabel": "Nombre del grupo",
"defaultGroupName": "El Grupo Asombroso", "defaultGroupName": "El Grupo Asombroso",
"createGroupTitle": "Crear un grupo" "createGroupBtn": "Crear",
"copiedToClipboardNotification": "Copiado al portapapeles",
"addPeerTab": "Agregar Contacto",
"createGroupTab": "Crear un grupo",
"peerAddress": "Dirección",
"peerName": "Nombre",
"groupName": "Nombre del grupo",
"server": "Servidor",
"invitation": "Invitación",
"groupAddr": "Dirección",
"addPeer": "Agregar Contacto",
"createGroup": "Crear perfil",
"joinGroup": "Únete al grupo",
"newBulletinLabel": "Nuevo Boletín",
"postNewBulletinLabel": "Publicar nuevo boletín",
"titlePlaceholder": "título...",
"pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...",
"blocked": "Bloqueado",
"search": "Búsqueda...",
"invitationLabel": "Invitación",
"serverInfo": "Información del servidor",
"serverConnectivityConnected": "Servidor conectado",
"serverConnectivityDisconnected": "Servidor desconectado",
"serverSynced": "Sincronizado",
"serverNotSynced": "Fuera de sincronización con el servidor",
"viewServerInfo": "Información del servidor",
"saveBtn": "Guardar",
"inviteToGroupLabel": "Invitar al grupo",
"inviteBtn": "Invitar",
"deleteBtn": "Eliminar",
"update": "Actualizar",
"searchList": "Buscar en la lista",
"addListItemBtn": "Agregar artículo",
"membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo",
"dmTooltip": "Haz clic para enviar mensaje directo",
"copyBtn": "Copiar",
"copiedClipboardNotification": "Copiado al portapapeles",
"newGroupBtn": "Crear un nuevo grupo de chat",
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
"acceptGroupBtn": "Aceptar",
"rejectGroupBtn": "Rechazar",
"listsBtn": "Listas",
"puzzleGameBtn": "Juego de rompecabezas",
"addressLabel": "Dirección",
"displayNameLabel": "Nombre de Usuario",
"addProfileTitle": "Agregar nuevo perfil",
"editProfileTitle": "Editar perfil",
"profileName": "Nombre de Usuario",
"newProfile": "Nuevo perfil",
"editProfile": "Editar perfil",
"radioUsePassword": "Contraseña",
"noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados",
"password2Label": "Vuelve a ingresar tu contraseña",
"yourDisplayName": "Tu nombre de usuario",
"currentPasswordLabel": "Contraseña actual",
"password1Label": "Contraseña",
"passwordErrorEmpty": "El campo de contraseña no puede estar vacío",
"createProfileBtn": "Crear perfil",
"saveProfileBtn": "Guardar perfil",
"passwordErrorMatch": "Las contraseñas no coinciden",
"passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada",
"deleteProfileBtn": "Eliminar Perfil",
"deleteConfirmLabel": "Escribe ELIMINAR para confirmar",
"deleteProfileConfirmBtn": "Confirmar eliminar perfil",
"deleteConfirmText": "ELIMINAR",
"addNewProfileBtn": "Agregar nuevo perfil",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"yourProfiles": "Tus perfiles",
"yourServers": "Tus servidores",
"unlock": "Desbloquear",
"cwtchSettingsTitle": "Configuración de Cwtch",
"blockUnknownLabel": "Bloquear conexiones desconocidas",
"settingLanguage": "Idioma",
"localeEn": "Inglés",
"settingInterfaceZoom": "Nivel de zoom",
"largeTextLabel": "Grande",
"version": "Versión %1",
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor"
} }

View File

@ -1,74 +1,88 @@
{ {
"@@locale": "fr", "@@locale": "fr",
"@@last_modified": "2021-11-11T01:02:08+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"newMessagesLabel": "New Messages", "experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"localeRU": "Russian", "enableExperimentClickableLinks": "Enable Clickable Links",
"serverMetricsLabel": "Métriques du serveur",
"serverTotalMessagesLabel": "Nombre total de messages",
"serverConnectionsLabel": "Connexion",
"manageKnownServersShort": "Serveurs",
"manageKnownServersLong": "Gérer les serveurs connus",
"manageKnownServersButton": "Gérer les serveurs connus",
"importLocalServerSelectText": "Sélectionnez le serveur local",
"importLocalServerLabel": "Importer un serveur hébergé localement",
"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", "copyServerKeys": "Copier les clés",
"verfiyResumeButton": "Vérifier\/reprendre", "verfiyResumeButton": "Vérifier\/reprendre",
"fileCheckingStatus": "Vérification de l'état du téléchargement",
"fileInterrupted": "Interrompu",
"fileSavedTo": "Enregistré dans", "fileSavedTo": "Enregistré dans",
"fileInterrupted": "Interrompu",
"fileCheckingStatus": "Vérification de l'état du téléchargement",
"plainServerDescription": "Nous vous recommandons de protéger vos serveurs Cwtch par un mot de passe. Si vous ne définissez pas de mot de passe sur ce serveur, toute personne ayant accès à cet appareil peut être en mesure d'accéder aux informations concernant ce serveur, y compris les clés cryptographiques sensibles.", "plainServerDescription": "Nous vous recommandons de protéger vos serveurs Cwtch par un mot de passe. Si vous ne définissez pas de mot de passe sur ce serveur, toute personne ayant accès à cet appareil peut être en mesure d'accéder aux informations concernant ce serveur, y compris les clés cryptographiques sensibles.",
"encryptedServerDescription": "Le chiffrement dun serveur avec un mot de passe le protège des autres personnes qui peuvent également utiliser cet appareil. Les serveurs cryptés ne peuvent pas être déchiffrés, affichés ou accessibles tant que le mot de passe correct nest pas entré pour les déverrouiller.",
"deleteServerConfirmBtn": "Supprimer vraiment le serveur",
"deleteServerSuccess": "Le serveur a été supprimé avec succès",
"enterCurrentPasswordForDeleteServer": "Veuillez saisir le mot de passe actuel pour supprimer ce serveur", "enterCurrentPasswordForDeleteServer": "Veuillez saisir le mot de passe actuel pour supprimer ce serveur",
"copyAddress": "Copier l'adresse", "encryptedServerDescription": "Le chiffrement dun serveur avec un mot de passe le protège des autres personnes qui peuvent également utiliser cet appareil. Les serveurs cryptés ne peuvent pas être déchiffrés, affichés ou accessibles tant que le mot de passe correct nest pas entré pour les déverrouiller.",
"deleteServerSuccess": "Le serveur a été supprimé avec succès",
"deleteServerConfirmBtn": "Supprimer vraiment le serveur",
"unlockServerTip": "Veuillez créer ou déverrouiller un serveur pour commencer !",
"unlockProfileTip": "Veuillez créer ou déverrouiller un profil pour commencer !",
"settingServersDescription": "L'expérience des serveurs d'hébergement permet d'héberger et de gérer les serveurs Cwtch.", "settingServersDescription": "L'expérience des serveurs d'hébergement permet d'héberger et de gérer les serveurs Cwtch.",
"settingServers": "Serveurs d'hébergement", "settingServers": "Serveurs d'hébergement",
"enterServerPassword": "Entrez le mot de passe pour déverrouiller le serveur",
"unlockProfileTip": "Veuillez créer ou déverrouiller un profil pour commencer !",
"unlockServerTip": "Veuillez créer ou déverrouiller un serveur pour commencer !",
"addServerTooltip": "Ajouter un nouveau serveur",
"serversManagerTitleShort": "Serveurs", "serversManagerTitleShort": "Serveurs",
"serversManagerTitleLong": "Serveurs que vous hébergez", "serversManagerTitleLong": "Serveurs que vous hébergez",
"saveServerButton": "Enregistrer le serveur",
"serverAutostartDescription": "Contrôle si l'application lance automatiquement le serveur au démarrage.",
"serverAutostartLabel": "Démarrage automatique",
"serverEnabledDescription": "Démarrer ou arrêter le serveur", "serverEnabledDescription": "Démarrer ou arrêter le serveur",
"serverEnabled": "Serveur activé", "serverEnabled": "Serveur activé",
"serverDescriptionDescription": "Votre description du serveur est à des fins de gestion personnelle uniquement, elle ne sera jamais partagée.",
"serverDescriptionLabel": "Description du serveur", "serverDescriptionLabel": "Description du serveur",
"serverDescriptionDescription": "Votre description du serveur est à des fins de gestion personnelle uniquement, elle ne sera jamais partagée.",
"serverAutostartLabel": "Démarrage automatique",
"serverAutostartDescription": "Contrôle si l'application lance automatiquement le serveur au démarrage.",
"serverAddress": "Adresse du serveur", "serverAddress": "Adresse du serveur",
"saveServerButton": "Enregistrer le serveur",
"enterServerPassword": "Entrez le mot de passe pour déverrouiller le serveur",
"editServerTitle": "Modifier le serveur", "editServerTitle": "Modifier le serveur",
"copyAddress": "Copier l'adresse",
"addServerTooltip": "Ajouter un nouveau serveur",
"addServerTitle": "Ajouter un serveur", "addServerTitle": "Ajouter un serveur",
"titleManageProfilesShort": "Profils", "titleManageProfilesShort": "Profils",
"descriptionStreamerMode": "Si elle est activée, cette option donne un rendu visuel plus privé à l'application pour la diffusion en direct ou la présentation, par exemple, en masquant profil et adresses de contacts.", "descriptionStreamerMode": "Si elle est activée, cette option donne un rendu visuel plus privé à l'application pour la diffusion en direct ou la présentation, par exemple, en masquant profil et adresses de contacts.",
"descriptionFileSharing": "L'expérience de partage de fichiers vous permet d'envoyer et de recevoir des fichiers à partir de contacts et de groupes Cwtch. Notez que si vous partagez un fichier avec un groupe, les membres de ce groupe se connecteront avec vous directement via Cwtch pour le télécharger.",
"settingFileSharing": "Partage de fichiers",
"tooltipSendFile": "Envoyer le fichier", "tooltipSendFile": "Envoyer le fichier",
"messageFileOffered": "Contact vous propose de vous envoyer un fichier", "settingFileSharing": "Partage de fichiers",
"retrievingManifestMessage": "Récupération des informations sur le fichier...",
"openFolderButton": "Ouvrir le dossier",
"messageFileSent": "Vous avez envoyé un fichier", "messageFileSent": "Vous avez envoyé un fichier",
"messageFileOffered": "Contact vous propose de vous envoyer un fichier",
"messageEnableFileSharing": "Activez l'expérience de partage de fichiers pour afficher ce message.", "messageEnableFileSharing": "Activez l'expérience de partage de fichiers pour afficher ce message.",
"labelFilesize": "Taille", "labelFilesize": "Taille",
"labelFilename": "Nom de fichier", "labelFilename": "Nom de fichier",
"downloadFileButton": "Télécharger", "downloadFileButton": "Télécharger",
"openFolderButton": "Ouvrir le dossier", "descriptionFileSharing": "L'expérience de partage de fichiers vous permet d'envoyer et de recevoir des fichiers à partir de contacts et de groupes Cwtch. Notez que si vous partagez un fichier avec un groupe, les membres de ce groupe se connecteront avec vous directement via Cwtch pour le télécharger.",
"retrievingManifestMessage": "Récupération des informations sur le fichier...",
"streamerModeLabel": "Mode Streamer\/Présentation", "streamerModeLabel": "Mode Streamer\/Présentation",
"archiveConversation": "Archiver cette conversation", "addPeer": "Ajouter le contact",
"networkStatusConnecting": "Connexion au réseau et aux contacts...",
"profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.", "profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.",
"addPeerTab": "Ajouter un contact", "addPeerTab": "Ajouter un contact",
"addPeer": "Ajouter le contact",
"peerNotOnline": "Le contact est hors ligne. Les applications ne peuvent pas être utilisées pour le moment.", "peerNotOnline": "Le contact est hors ligne. Les applications ne peuvent pas être utilisées pour le moment.",
"peerBlockedMessage": "Le contact est bloqué", "peerBlockedMessage": "Le contact est bloqué",
"peerOfflineMessage": "Le contact est hors ligne, les messages ne peuvent pas être transmis pour le moment.", "peerOfflineMessage": "Le contact est hors ligne, les messages ne peuvent pas être transmis pour le moment.",
"blockBtn": "Bloquer le contact", "blockBtn": "Bloquer le contact",
"savePeerHistory": "Enregistrer l'historique", "savePeerHistory": "Enregistrer l'historique",
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au contact.",
"dontSavePeerHistory": "Supprimer l'historique", "dontSavePeerHistory": "Supprimer l'historique",
"unblockBtn": "Débloquer le contact", "unblockBtn": "Débloquer le contact",
"blockUnknownLabel": "Bloquer les pairs inconnus", "blockUnknownLabel": "Bloquer les pairs inconnus",
"blockUnknownConnectionsEnabledDescription": "Les connexions provenant de contacts inconnus sont bloquées. Vous pouvez modifier cela dans les paramètres", "blockUnknownConnectionsEnabledDescription": "Les connexions provenant de contacts inconnus sont bloquées. Vous pouvez modifier cela dans les paramètres",
"networkStatusConnecting": "Connexion au réseau et aux contacts...", "archiveConversation": "Archiver cette conversation",
"showMessageButton": "Afficher le message",
"blockedMessageMessage": "Ce message provient d'un profil que vous avez bloqué.", "blockedMessageMessage": "Ce message provient d'un profil que vous avez bloqué.",
"showMessageButton": "Afficher le message",
"placeholderEnterMessage": "saisissez un message", "placeholderEnterMessage": "saisissez un message",
"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.",
"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.", "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", "addContactConfirm": "Ajouter le contact %1",
"addContact": "Ajouter le contact",
"contactGoto": "Aller à la conversation avec %1", "contactGoto": "Aller à la conversation avec %1",
"addContact": "Ajouter le contact",
"settingUIColumnOptionSame": "Même réglage que pour le mode portrait", "settingUIColumnOptionSame": "Même réglage que pour le mode portrait",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",
@ -76,128 +90,158 @@
"settingUIColumnLandscape": "Colonnes de l'interface utilisateur en mode paysage", "settingUIColumnLandscape": "Colonnes de l'interface utilisateur en mode paysage",
"settingUIColumnPortrait": "Colonnes de l'interface utilisateur en mode portrait", "settingUIColumnPortrait": "Colonnes de l'interface utilisateur en mode portrait",
"localePl": "Polonais", "localePl": "Polonais",
"tooltipRemoveThisQuotedMessage": "Supprimer le message cité.",
"tooltipReplyToThisMessage": "Répondre à ce message", "tooltipReplyToThisMessage": "Répondre à ce message",
"tooltipRejectContactRequest": "Refuser cette demande de contact", "tooltipRemoveThisQuotedMessage": "Supprimer le message cité.",
"tooltipAcceptContactRequest": "Acceptez cette demande de contact.", "deleteProfileConfirmBtn": "Supprimer vraiment le profil ?",
"notificationNewMessageFromGroup": "Nouveau message dans un groupe !", "groupNameLabel": "Nom du groupe",
"notificationNewMessageFromPeer": "Nouveau message d'un contact !", "defaultGroupName": "Un groupe génial",
"tooltipHidePassword": "Masquer le mot de passe", "inviteToGroupLabel": "Inviter au groupe",
"tooltipShowPassword": "Afficher le mot de passe", "membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être représentatives de l'ensemble des membres du groupe.",
"serverNotSynced": "Synchronisation des nouveaux messages (Cela peut prendre un certain temps)...",
"groupInviteSettingsWarning": "Vous avez été invité à rejoindre un groupe ! Veuillez activer l'expérience de discussion de groupe dans les paramètres pour afficher cette invitation.",
"shutdownCwtchAction": "Arrêt de Cwtch",
"shutdownCwtchDialog": "Êtes-vous sûr de vouloir arrêter Cwtch ? Ceci fermera toutes les connexions, et quittera l'application.",
"shutdownCwtchDialogTitle": "Arrêter Cwtch ?",
"shutdownCwtchTooltip": "Arrêt de Cwtch", "shutdownCwtchTooltip": "Arrêt de Cwtch",
"malformedMessage": "Message mal formé", "shutdownCwtchAction": "Arrêt de Cwtch",
"profileDeleteSuccess": "Le profil a été supprimé avec succès", "deleteBtn": "Effacer",
"debugLog": "Activer le journal de la console de débogage", "acknowledgedLabel": "Accusé de réception",
"torNetworkStatus": "Statut du réseau Tor", "zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)",
"addContactFirst": "Ajoutez ou choisissez un contact pour commencer à discuter.", "localeIt": "Italien",
"createProfileToBegin": "Veuillez créer ou déverrouiller un profil pour commencer", "versionTor": "Version %1 avec tor %2",
"nickChangeSuccess": "Le pseudo du profil a été modifié avec succès", "version": "Version %1",
"addServerFirst": "Vous devez ajouter un serveur avant de pouvoir créer un groupe.", "builddate": "Construit le : %2",
"deleteProfileSuccess": "Le profil a été supprimé avec succès", "versionBuilddate": "Version : %1 Construite le : %2",
"sendInvite": "Envoyer une invitation à un contact ou à un groupe", "tooltipAcceptContactRequest": "Acceptez cette demande de contact.",
"sendMessage": "Envoyer un message", "tooltipRejectContactRequest": "Refuser cette demande de contact",
"cancel": "Annuler", "addNewItem": "Ajouter un nouvel élément à la liste",
"resetTor": "Réinitialiser", "localeEs": "Espagnol",
"torStatus": "Statut de Tor", "todoPlaceholder": "À faire...",
"torVersion": "Version de Tor",
"sendAnInvitation": "Vous avez envoyé une invitation pour : ",
"contactSuggestion": "Il s'agit d'une suggestion de contact pour : ",
"rejected": "Rejeté !",
"accepted": "Accepté !",
"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.",
"newPassword": "Nouveau mot de passe",
"yesLeave": "Oui, quittez cette conversation",
"reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.",
"leaveGroup": "Quittez cette conversation",
"inviteToGroup": "Vous avez été invité à rejoindre un groupe :",
"pasteAddressToAddContact": "Collez une adresse cwtch, une invitation ou un ensemble de clés ici pour ajouter une nouvelle conversation", "pasteAddressToAddContact": "Collez une adresse cwtch, une invitation ou un ensemble de clés ici pour ajouter une nouvelle conversation",
"tooltipAddContact": "Ajouter un nouveau contact ou une nouvelle conversation", "addListItem": "Ajouter un nouvel élément de liste",
"titleManageContacts": "Conversations", "cycleMorphsAndroid": "Cliquez pour faire défiler les morphes.\n Appuyez longuement pour réinitialiser.",
"titleManageServers": "Gérer les serveurs", "cycleMorphsDesktop": "Cliquez pour faire défiler les morphes.\n Faites un clic droit pour réinitialiser.",
"dateNever": "Jamais", "debugLog": "Activer le journal de la console de débogage",
"joinGroupTab": "Rejoindre un groupe",
"createGroupTab": "Créer un groupe",
"peerAddress": "Adresse",
"peerName": "Nom",
"groupName": "Nom du groupe",
"server": "Serveur",
"invitation": "Invitation",
"cycleCatsAndroid": "Cliquez pour faire défiler les catégories.\nAppuyez longuement pour réinitialiser.",
"cycleCatsDesktop": "Cliquez pour parcourir la catégorie.\n Faites un clic droit pour réinitialiser.",
"cycleColoursAndroid": "Cliquez pour faire défiler les couleurs.\nAppuyez longuement pour réinitialiser.",
"groupAddr": "Adresse",
"createGroup": "Créer un groupe",
"joinGroup": "Rejoindre le groupe",
"blocked": "Bloqué",
"cycleColoursDesktop": "Cliquez pour faire défiler les couleurs.\nCliquez avec le bouton droit de la souris pour réinitialiser.",
"search": "Recherche...",
"serverInfo": "Informations sur le serveur",
"serverConnectivityConnected": "Serveur connecté",
"serverConnectivityDisconnected": "Serveur déconnecté",
"serverSynced": "Synchronisé",
"serverNotSynced": "Synchronisation des nouveaux messages (Cela peut prendre un certain temps)...",
"viewServerInfo": "Informations sur le serveur",
"update": "Mise à jour",
"searchList": "Liste de recherche",
"addListItemBtn": "Ajouter un élément",
"addProfileTitle": "Ajouter un nouveau profil",
"editProfileTitle": "Modifier le profil",
"profileName": "Pseudo",
"defaultProfileName": "Alice",
"newProfile": "Nouveau profil",
"deleteConfirmText": "SUPPRIMER",
"deleteConfirmLabel": "Tapez SUPPRIMER pour confirmer",
"addNewProfileBtn": "Ajouter un nouveau profil",
"enterProfilePassword": "Entrez un mot de passe pour consulter vos profils",
"editProfile": "Modifier le profil",
"radioUsePassword": "Mot de passe",
"radioNoPassword": "Non chiffré (pas de mot de passe)",
"saveProfileBtn": "Sauvegarder le profil",
"passwordErrorMatch": "Les mots de passe ne correspondent pas",
"passwordChangeError": "Erreur lors de la modification du mot de passe : le mot de passe fourni est rejeté",
"deleteProfileBtn": "Supprimer le profil",
"password": "Mot de passe",
"error0ProfilesLoadedForPassword": "Aucun profils chargés avec ce mot de passe",
"yourProfiles": "Vos profils",
"yourServers": "Vos serveurs",
"unlock": "Déverrouiller",
"settingLanguage": "Langue",
"localeEn": "Anglais",
"localeFr": "Français",
"localePt": "Portugais",
"localeDe": "Allemand",
"settingInterfaceZoom": "Niveau de zoom",
"settingTheme": "Thème",
"themeLight": "Clair",
"themeDark": "Sombre",
"experimentsEnabled": "Activer les expériences",
"dateLastYear": "L'année dernière", "dateLastYear": "L'année dernière",
"dateYesterday": "Hier", "dateNever": "Jamais",
"dateLastMonth": "Le mois dernier", "titleManageServers": "Gérer les serveurs",
"dateRightNow": "Maintenant", "inviteToGroup": "Vous avez été invité à rejoindre un groupe :",
"successfullAddedContact": "Ajouté avec succès ", "leaveGroup": "Quittez cette conversation",
"descriptionBlockUnknownConnections": "Si elle est activée, cette option fermera automatiquement les connexions des utilisateurs de Cwtch qui n'ont pas été ajoutés à votre liste de contacts.", "reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.",
"descriptionExperimentsGroups": "L'expérience de groupe permet à Cwtch de se connecter à une infrastructure de serveurs non fiables pour faciliter la communication avec plus d'un contact.", "yesLeave": "Oui, quittez cette conversation",
"descriptionExperiments": "Les expériences de Cwtch sont des fonctionnalités optionnelles et facultatives qui ajoutent des fonctionnalités supplémentaires à Cwtch et qui peuvent avoir des considérations de confidentialité différentes de celles du chat traditionnel résistant aux métadonnées 1:1, par exemple le chat de groupe, l'intégration de robots, etc.", "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.",
"titleManageProfiles": "Gérer les profils Cwtch", "yourDisplayName": "Pseudo",
"tooltipUnlockProfiles": "Déverrouillez les profils chiffrés en saisissant leur mot de passe.", "currentPasswordLabel": "Mot de passe actuel",
"tooltipOpenSettings": "Ouvrez le volet des paramètres", "password1Label": "Mot de passe",
"password2Label": "Saisissez à nouveau le mot de passe",
"passwordErrorEmpty": "Le mot de passe ne peut pas être vide",
"createProfileBtn": "Créer un profil",
"loadingTor": "Chargement de tor...",
"viewGroupMembershipTooltip": "Afficher les membres du groupe",
"networkStatusDisconnected": "Déconnecté d'Internet, vérifiez votre connexion",
"networkStatusAttemptingTor": "Tentative de connexion au réseau Tor",
"networkStatusOnline": "En ligne",
"newConnectionPaneTitle": "Nouvelle connexion",
"enableGroups": "Activer la discussion de groupe",
"enterCurrentPasswordForDelete": "Veuillez entrer le mot de passe actuel pour supprimer ce profil.",
"conversationSettings": "Paramètres de conversation",
"invalidImportString": "Chaîne d'importation non valide", "invalidImportString": "Chaîne d'importation non valide",
"contactAlreadyExists": "Le contact existe déjà", "contactAlreadyExists": "Le contact existe déjà",
"conversationSettings": "Paramètres de conversation", "tooltipOpenSettings": "Ouvrez le volet des paramètres",
"enterCurrentPasswordForDelete": "Veuillez entrer le mot de passe actuel pour supprimer ce profil.", "tooltipAddContact": "Ajouter un nouveau contact ou une nouvelle conversation",
"enableGroups": "Activer la discussion de groupe", "titleManageContacts": "Conversations",
"experimentsEnabled": "Activer les expériences", "tooltipUnlockProfiles": "Déverrouillez les profils chiffrés en saisissant leur mot de passe.",
"localeIt": "Italien", "titleManageProfiles": "Gérer les profils Cwtch",
"localeEs": "Espagnol", "descriptionExperiments": "Les expériences de Cwtch sont des fonctionnalités optionnelles et facultatives qui ajoutent des fonctionnalités supplémentaires à Cwtch et qui peuvent avoir des considérations de confidentialité différentes de celles du chat traditionnel résistant aux métadonnées 1:1, par exemple le chat de groupe, l'intégration de robots, etc.",
"addListItem": "Ajouter un nouvel élément de liste", "descriptionExperimentsGroups": "L'expérience de groupe permet à Cwtch de se connecter à une infrastructure de serveurs non fiables pour faciliter la communication avec plus d'un contact.",
"addNewItem": "Ajouter un nouvel élément à la liste", "descriptionBlockUnknownConnections": "Si elle est activée, cette option fermera automatiquement les connexions des utilisateurs de Cwtch qui n'ont pas été ajoutés à votre liste de contacts.",
"todoPlaceholder": "À faire...", "successfullAddedContact": "Ajouté avec succès ",
"newConnectionPaneTitle": "Nouvelle connexion", "dateRightNow": "Maintenant",
"networkStatusOnline": "En ligne", "dateLastMonth": "Le mois dernier",
"networkStatusAttemptingTor": "Tentative de connexion au réseau Tor", "dateYesterday": "Hier",
"networkStatusDisconnected": "Déconnecté d'Internet, vérifiez votre connexion", "newPassword": "Nouveau mot de passe",
"viewGroupMembershipTooltip": "Afficher les membres du groupe", "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.",
"loadingTor": "Chargement de tor...", "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",
"resetTor": "Réinitialiser",
"cancel": "Annuler",
"sendMessage": "Envoyer un message",
"sendInvite": "Envoyer une invitation à un contact ou à un groupe",
"deleteProfileSuccess": "Le profil a été supprimé avec succès",
"addServerFirst": "Vous devez ajouter un serveur avant de pouvoir créer un groupe.",
"nickChangeSuccess": "Le pseudo du profil a été modifié avec succès",
"createProfileToBegin": "Veuillez créer ou déverrouiller un profil pour commencer",
"addContactFirst": "Ajoutez ou choisissez un contact pour commencer à discuter.",
"torNetworkStatus": "Statut du réseau Tor",
"profileDeleteSuccess": "Le profil a été supprimé avec succès",
"malformedMessage": "Message mal formé",
"shutdownCwtchDialogTitle": "Arrêter Cwtch ?",
"shutdownCwtchDialog": "Êtes-vous sûr de vouloir arrêter Cwtch ? Ceci fermera toutes les connexions, et quittera l'application.",
"groupInviteSettingsWarning": "Vous avez été invité à rejoindre un groupe ! Veuillez activer l'expérience de discussion de groupe dans les paramètres pour afficher cette invitation.",
"tooltipShowPassword": "Afficher le mot de passe",
"tooltipHidePassword": "Masquer le mot de passe",
"notificationNewMessageFromPeer": "Nouveau message d'un contact !",
"notificationNewMessageFromGroup": "Nouveau message dans un groupe !",
"smallTextLabel": "Petit", "smallTextLabel": "Petit",
"defaultScalingText": "Taille par défaut du texte (échelle:", "defaultScalingText": "Taille par défaut du texte (échelle:",
"builddate": "Construit le : %2",
"version": "Version %1",
"versionTor": "Version %1 avec tor %2",
"themeDark": "Sombre",
"themeLight": "Clair",
"settingTheme": "Thème",
"largeTextLabel": "Large", "largeTextLabel": "Large",
"settingInterfaceZoom": "Niveau de zoom",
"localeDe": "Allemand",
"localePt": "Portugais",
"localeFr": "Français",
"localeEn": "Anglais",
"settingLanguage": "Langue",
"zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)",
"versionBuilddate": "Version : %1 Construite le : %2",
"cwtchSettingsTitle": "Préférences Cwtch", "cwtchSettingsTitle": "Préférences Cwtch",
"unlock": "Déverrouiller",
"yourServers": "Vos serveurs",
"yourProfiles": "Vos profils",
"error0ProfilesLoadedForPassword": "Aucun profils chargés avec ce mot de passe",
"password": "Mot de passe",
"enterProfilePassword": "Entrez un mot de passe pour consulter vos profils",
"addNewProfileBtn": "Ajouter un nouveau profil",
"deleteConfirmText": "SUPPRIMER",
"deleteProfileConfirmBtn": "Supprimer vraiment le profil ?",
"deleteConfirmLabel": "Tapez SUPPRIMER pour confirmer",
"deleteProfileBtn": "Supprimer le profil",
"passwordChangeError": "Erreur lors de la modification du mot de passe : le mot de passe fourni est rejeté",
"passwordErrorMatch": "Les mots de passe ne correspondent pas",
"saveProfileBtn": "Sauvegarder le profil",
"createProfileBtn": "Créer un profil",
"passwordErrorEmpty": "Le mot de passe ne peut pas être vide",
"password2Label": "Saisissez à nouveau le mot de passe",
"password1Label": "Mot de passe",
"currentPasswordLabel": "Mot de passe actuel",
"yourDisplayName": "Pseudo",
"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.",
"radioNoPassword": "Non chiffré (pas de mot de passe)",
"radioUsePassword": "Mot de passe",
"copiedToClipboardNotification": "Copié dans le presse-papier", "copiedToClipboardNotification": "Copié dans le presse-papier",
"copyBtn": "Copier",
"editProfile": "Modifier le profil",
"newProfile": "Nouveau profil",
"defaultProfileName": "Alice",
"profileName": "Pseudo",
"editProfileTitle": "Modifier le profil",
"addProfileTitle": "Ajouter un nouveau profil",
"deleteBtn": "Effacer",
"saveBtn": "Sauvegarder", "saveBtn": "Sauvegarder",
"displayNameLabel": "Pseudo", "displayNameLabel": "Pseudo",
"addressLabel": "Adresse", "addressLabel": "Adresse",
@ -210,46 +254,16 @@
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe", "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe",
"newGroupBtn": "Créer un nouveau groupe", "newGroupBtn": "Créer un nouveau groupe",
"copiedClipboardNotification": "Copié dans le presse-papier", "copiedClipboardNotification": "Copié dans le presse-papier",
"copyBtn": "Copier",
"pendingLabel": "En attente", "pendingLabel": "En attente",
"acknowledgedLabel": "Accusé de réception",
"couldNotSendMsgError": "Impossible d'envoyer ce message", "couldNotSendMsgError": "Impossible d'envoyer ce message",
"dmTooltip": "Envoyer un message privé", "dmTooltip": "Envoyer un message privé",
"membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être représentatives de l'ensemble des membres du groupe.",
"addListItemBtn": "Ajouter un élément",
"searchList": "Liste de recherche",
"update": "Mise à jour",
"inviteBtn": "Invitation", "inviteBtn": "Invitation",
"inviteToGroupLabel": "Inviter au groupe",
"groupNameLabel": "Nom du groupe",
"viewServerInfo": "Informations sur le serveur",
"serverSynced": "Synchronisé",
"serverConnectivityDisconnected": "Serveur déconnecté",
"serverConnectivityConnected": "Serveur connecté",
"serverInfo": "Informations sur le serveur",
"invitationLabel": "Invitation", "invitationLabel": "Invitation",
"serverLabel": "Serveur", "serverLabel": "Serveur",
"search": "Recherche...",
"cycleColoursDesktop": "Cliquez pour faire défiler les couleurs.\nCliquez avec le bouton droit de la souris pour réinitialiser.",
"cycleColoursAndroid": "Cliquez pour faire défiler les couleurs.\nAppuyez longuement pour réinitialiser.",
"cycleMorphsDesktop": "Cliquez pour faire défiler les morphes.\n Faites un clic droit pour réinitialiser.",
"cycleMorphsAndroid": "Cliquez pour faire défiler les morphes.\n Appuyez longuement pour réinitialiser.",
"cycleCatsDesktop": "Cliquez pour parcourir la catégorie.\n Faites un clic droit pour réinitialiser.",
"cycleCatsAndroid": "Cliquez pour faire défiler les catégories.\nAppuyez longuement pour réinitialiser.",
"blocked": "Bloqué",
"titlePlaceholder": "titre...", "titlePlaceholder": "titre...",
"postNewBulletinLabel": "Envoyer un nouveau bulletin", "postNewBulletinLabel": "Envoyer un nouveau bulletin",
"newBulletinLabel": "Nouveau bulletin", "newBulletinLabel": "Nouveau bulletin",
"joinGroup": "Rejoindre le groupe",
"createGroup": "Créer un groupe",
"groupAddr": "Adresse",
"invitation": "Invitation",
"server": "Serveur",
"groupName": "Nom du groupe",
"peerName": "Nom",
"peerAddress": "Adresse",
"joinGroupTab": "Rejoindre un groupe",
"createGroupTab": "Créer un groupe",
"createGroupBtn": "Créer", "createGroupBtn": "Créer",
"defaultGroupName": "Un groupe génial",
"createGroupTitle": "Créer un groupe" "createGroupTitle": "Créer un groupe"
} }

View File

@ -1,255 +1,269 @@
{ {
"@@locale": "it", "@@locale": "it",
"@@last_modified": "2021-11-11T01:02:08+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"newMessagesLabel": "New Messages", "experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"localeRU": "Russian", "enableExperimentClickableLinks": "Enable Clickable Links",
"copyServerKeys": "Copy keys", "serverConnectionsLabel": "Connection",
"verfiyResumeButton": "Verify\/resume", "serverTotalMessagesLabel": "Total Messages",
"fileCheckingStatus": "Checking download status", "serverMetricsLabel": "Server Metrics",
"fileInterrupted": "Interrupted", "manageKnownServersShort": "Servers",
"fileSavedTo": "Saved to", "manageKnownServersLong": "Manage Known Servers",
"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.", "displayNameTooltip": "Please enter a display name",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.", "manageKnownServersButton": "Manage Known Servers",
"deleteServerConfirmBtn": "Really delete server", "fieldDescriptionLabel": "Description",
"deleteServerSuccess": "Successfully deleted server", "groupsOnThisServerLabel": "Groups I am in hosted on this server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", "importLocalServerButton": "Import %1",
"copyAddress": "Copy Address", "importLocalServerSelectText": "Select Local Server",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers", "importLocalServerLabel": "Import a locally hosted server",
"settingServers": "Hosting Servers", "descriptionStreamerMode": "Se attivata, questa opzione rende l'applicazione visivamente più privata per lo streaming o la presentazione, ad esempio nascondendo il profilo e gli indirizzi di contatto",
"enterServerPassword": "Enter password to unlock server", "retrievingManifestMessage": "Recupero delle informazioni sul file in corso...",
"unlockProfileTip": "Please create or unlock a profile to begin!", "openFolderButton": "Apri cartella",
"unlockServerTip": "Please create or unlock a server to begin!", "downloadFileButton": "Scarica",
"addServerTooltip": "Add new server", "labelFilename": "Nome del file",
"serversManagerTitleShort": "Servers", "labelFilesize": "Dimensione",
"serversManagerTitleLong": "Servers You Host", "messageEnableFileSharing": "Abilita l'esperimento di condivisione dei file per visualizzare questo messaggio.",
"saveServerButton": "Save Server", "messageFileSent": "Hai inviato un file",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start", "titleManageProfilesShort": "Profili",
"serverAutostartLabel": "Autostart", "addServerTitle": "Aggiungi server",
"serverEnabledDescription": "Start or stop the server", "tooltipReplyToThisMessage": "Rispondi a questo messaggio",
"serverEnabled": "Server Enabled", "tooltipRemoveThisQuotedMessage": "Rimuovi il messaggio citato.",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared", "localePl": "Polacco",
"serverDescriptionLabel": "Server Description", "settingUIColumnPortrait": "Colonne dell'interfaccia utente in modalità verticale",
"serverAddress": "Server Address", "settingUIColumnLandscape": "Colonne dell'interfaccia utente in modalità orizzontale",
"editServerTitle": "Edit Server", "settingUIColumnSingle": "Singola",
"addServerTitle": "Add Server", "settingUIColumnDouble12Ratio": "Doppia (1:2)",
"titleManageProfilesShort": "Profiles", "settingUIColumnDouble14Ratio": "Doppia (1:4)",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses", "settingUIColumnOptionSame": "Stessa impostazione della modalità verticale",
"descriptionFileSharing": "L'esperimento di condivisione dei file ti consente di inviare e ricevere file dai contatti e dai gruppi di Cwtch. Tieni presente che la condivisione di un file con un gruppo farà sì che i membri di quel gruppo si colleghino con te direttamente su Cwtch per scaricarlo.", "contactGoto": "Vai alla conversazione con %1",
"settingFileSharing": "Condivisione file", "addContact": "Aggiungi contatto",
"tooltipSendFile": "Invia file", "addContactConfirm": "Aggiungi %1 come contatto",
"encryptedProfileDescription": "Criptare un profilo con una password lo protegge da altre persone che potrebbero usare questo dispositivo. I profili criptati non possono essere decriptati, visualizzati o accessibili finché non viene inserita la password corretta per sbloccarli.",
"plainServerDescription": "Ti raccomandiamo di proteggere i tuoi server Cwtch con una password. Se non imposti una password su questo server, chiunque abbia accesso a questo dispositivo potrebbe essere in grado di accedere alle relativ informazioni, compresi dati sensibili come le chiavi crittografiche.",
"plainProfileDescription": "Ti raccomandiamo di proteggere i tuoi profili Cwtch con una password. Se non imposti una password su questo profilo, chiunque abbia accesso a questo dispositivo potrebbe essere in grado di accedere alle relative informazioni, compresi contatti, messaggi e altri dati sensibili come le chiavi crittografiche.",
"placeholderEnterMessage": "Scrivi un messaggio...",
"blockedMessageMessage": "Questo messaggio proviene da un profilo che hai bloccato.",
"showMessageButton": "Mostra il messaggio",
"blockUnknownConnectionsEnabledDescription": "Le connessioni da contatti sconosciuti sono bloccate. Puoi modificare questa impostazione in Impostazioni",
"archiveConversation": "Archivia questa conversazione",
"streamerModeLabel": "Modalità Streamer\/Presentazione",
"editServerTitle": "Modifica il server",
"serverAddress": "Indirizzo del server",
"serverDescriptionLabel": "Descrizione del server",
"serverDescriptionDescription": "La tua descrizione del server solo per gestione personale, non sarà mai condivisa",
"serverEnabled": "Server abilitato",
"serverEnabledDescription": "Avvia o arresta il server",
"serverAutostartLabel": "Avvio automatico",
"serverAutostartDescription": "Controlla se l'applicazione avvierà automaticamente il server all'avvio",
"saveServerButton": "Salva il server",
"serversManagerTitleLong": "Server che gestisci",
"serversManagerTitleShort": "Gestisci i server",
"addServerTooltip": "Aggiungi nuovo server",
"unlockServerTip": "Crea o sblocca un server per iniziare!",
"unlockProfileTip": "Crea o sblocca un profilo per iniziare!",
"enterServerPassword": "Inserisci la password per sbloccare il server",
"settingServers": "Server di hosting",
"settingServersDescription": "L'esperimento dei server di hosting permette di allocare e gestire i server Cwtch",
"copyAddress": "Copia indirizzo",
"enterCurrentPasswordForDeleteServer": "Inserisci la password attuale per eliminare questo server",
"deleteServerSuccess": "Server eliminato con successo",
"deleteServerConfirmBtn": "Elimina davvero il server",
"encryptedServerDescription": "Criptare un server con una password lo protegge da altre persone che potrebbero usare questo dispositivo. I server criptati non possono essere decriptati, visualizzati o accessibili finché non viene inserita la password corretta per sbloccarli.",
"fileSavedTo": "Salvato in",
"fileInterrupted": "Interrotto",
"fileCheckingStatus": "Controllo dello stato del download",
"verfiyResumeButton": "Verifica\/riprendi",
"copyServerKeys": "Copia chiavi",
"localeRU": "Russo",
"newMessagesLabel": "Nuovi messaggi",
"messageFileOffered": "Il contatto offre l'invio di un file", "messageFileOffered": "Il contatto offre l'invio di un file",
"messageFileSent": "You sent a file", "tooltipSendFile": "Invia file",
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.", "settingFileSharing": "Condivisione file",
"labelFilesize": "Size", "descriptionFileSharing": "L'esperimento di condivisione dei file ti consente di inviare e ricevere file dai contatti e dai gruppi di Cwtch. Tieni presente che la condivisione di un file con un gruppo farà sì che i membri di quel gruppo si colleghino con te direttamente su Cwtch per scaricarlo.",
"labelFilename": "Filename", "experimentsEnabled": "Abilita esperimenti",
"downloadFileButton": "Download",
"openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...",
"streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
"addPeerTab": "Aggiungi un peer",
"addPeer": "Aggiungi peer",
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"peerBlockedMessage": "Il peer è bloccato",
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
"blockBtn": "Blocca il peer",
"savePeerHistory": "Salva cronologia peer",
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
"dontSavePeerHistory": "Elimina cronologia dei peer",
"unblockBtn": "Sblocca il peer",
"blockUnknownLabel": "Blocca peer sconosciuti",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
"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": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
"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": "Rifiuta questa richiesta di contatto",
"tooltipAcceptContactRequest": "Accetta questa richiesta di contatto.",
"notificationNewMessageFromGroup": "Nuovo messaggio in un gruppo!",
"notificationNewMessageFromPeer": "Nuovo messaggio da un contatto!",
"tooltipHidePassword": "Nascondi la password",
"tooltipShowPassword": "Mostra la password",
"serverNotSynced": "Sincronizzazione nuovi messaggi (l'operazione può richiedere del tempo)...", "serverNotSynced": "Sincronizzazione nuovi messaggi (l'operazione può richiedere del tempo)...",
"groupInviteSettingsWarning": "Sei stato invitato ad unirti ad un gruppo! Abilita l'Esperimento di chat di gruppo in Impostazioni per visualizzare questo Invito.",
"shutdownCwtchAction": "Chiudi Cwtch",
"shutdownCwtchDialog": "Sei sicuro di voler chiudere Cwtch? Questo chiuderà tutte le connessioni e uscirà dall'applicazione.",
"shutdownCwtchDialogTitle": "Chiudi Cwtch?",
"shutdownCwtchTooltip": "Chiudi Cwtch",
"malformedMessage": "Messaggio non valido",
"profileDeleteSuccess": "Profilo eliminato con successo",
"debugLog": "Attiva la registrazione del debug della console",
"torNetworkStatus": "Stato della rete Tor",
"addContactFirst": "Aggiungi o scegli un contatto per iniziare a chattare.",
"createProfileToBegin": "Crea o sblocca un profilo per iniziare",
"nickChangeSuccess": "Nickname del profilo modificato con successo",
"addServerFirst": "È necessario aggiungere un server prima di poter creare un gruppo",
"deleteProfileSuccess": "Profilo eliminato con successo",
"sendInvite": "Invia un invito a un contatto o a un gruppo",
"sendMessage": "Invia messaggio",
"cancel": "Annulla",
"resetTor": "Resettare",
"torStatus": "Stato di Tor",
"torVersion": "Versione di Tor",
"sendAnInvitation": "Hai inviato un invito per:",
"contactSuggestion": "Questo è un suggerimento di contatto per:",
"rejected": "Rifiutato!",
"accepted": "Accettato!",
"chatHistoryDefault": "Questa conversazione sarà cancellata quando Cwtch sarà chiuso! La cronologia dei messaggi può essere abilitata per ogni conversazione tramite il menu Impostazioni in alto a destra.",
"newPassword": "Nuova password",
"yesLeave": "Sì, lascia questa conversazione",
"reallyLeaveThisGroupPrompt": "Uscire da questa conversazione? Tutti i messaggi e gli attributi verranno eliminati.",
"leaveGroup": "Lascia questa conversazione",
"inviteToGroup": "Hai ricevuto un invito a unirti a un gruppo:",
"pasteAddressToAddContact": "Incolla qui un indirizzo cwtch, un invito o un mazzo di chiavi per aggiungere una nuova conversazione", "pasteAddressToAddContact": "Incolla qui un indirizzo cwtch, un invito o un mazzo di chiavi per aggiungere una nuova conversazione",
"tooltipAddContact": "Aggiungi un nuovo contatto o conversazione", "enableGroups": "Abilita la chat di gruppo",
"titleManageContacts": "Conversazioni", "enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.",
"titleManageServers": "Gestisci i server", "conversationSettings": "Impostazioni di conversazione",
"dateNever": "Mai",
"dateLastYear": "L'anno scorso",
"dateYesterday": "Ieri",
"dateLastMonth": "Mese scorso",
"dateRightNow": "Ora",
"successfullAddedContact": "Aggiunto con successo ",
"descriptionBlockUnknownConnections": "Se attivata, questa opzione chiuderà automaticamente le connessioni degli utenti Cwtch che non sono stati aggiunti alla tua lista di contatti.",
"descriptionExperimentsGroups": "L'esperimento di gruppo permette a Cwtch di connettersi con un'infrastruttura server non fidata per facilitare la comunicazione con più di un contatto.",
"descriptionExperiments": "Gli esperimenti di Cwtch sono opzioni a scelta che aggiungono a Cwtch funzionalità che possono avere diverse considerazioni sulla privacy rispetto alla tradizionale chat 1:1 resistente ai metadati, ad esempio chat di gruppo, integrazione di bot ecc.",
"titleManageProfiles": "Gestisci i profili Cwtch",
"tooltipUnlockProfiles": "Sblocca i profili crittografati inserendo la loro password.",
"tooltipOpenSettings": "Aprire il pannello delle impostazioni",
"invalidImportString": "Importazione stringa non valida", "invalidImportString": "Importazione stringa non valida",
"contactAlreadyExists": "Il contatto esiste già", "contactAlreadyExists": "Il contatto esiste già",
"conversationSettings": "Impostazioni di conversazione", "tooltipOpenSettings": "Aprire il pannello delle impostazioni",
"enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.", "tooltipAddContact": "Aggiungi un nuovo contatto o conversazione",
"enableGroups": "Abilita la chat di gruppo", "titleManageContacts": "Conversazioni",
"experimentsEnabled": "Abilita esperimenti", "tooltipUnlockProfiles": "Sblocca i profili crittografati inserendo la loro password.",
"titleManageProfiles": "Gestisci i profili Cwtch",
"descriptionExperiments": "Gli esperimenti di Cwtch sono opzioni a scelta che aggiungono a Cwtch funzionalità che possono avere diverse considerazioni sulla privacy rispetto alla tradizionale chat 1:1 resistente ai metadati, ad esempio chat di gruppo, integrazione di bot ecc.",
"descriptionExperimentsGroups": "L'esperimento di gruppo permette a Cwtch di connettersi con un'infrastruttura server non fidata per facilitare la comunicazione con più di un contatto.",
"descriptionBlockUnknownConnections": "Se attivata, questa opzione chiuderà automaticamente le connessioni degli utenti Cwtch che non sono stati aggiunti alla tua lista di contatti.",
"successfullAddedContact": "Aggiunto con successo ",
"dateRightNow": "Ora",
"dateLastMonth": "Mese scorso",
"dateYesterday": "Ieri",
"dateLastYear": "L'anno scorso",
"dateNever": "Mai",
"titleManageServers": "Gestisci i server",
"inviteToGroup": "Hai ricevuto un invito a unirti a un gruppo:",
"leaveGroup": "Lascia questa conversazione",
"reallyLeaveThisGroupPrompt": "Uscire da questa conversazione? Tutti i messaggi e gli attributi verranno eliminati.",
"yesLeave": "Sì, lascia questa conversazione",
"newPassword": "Nuova password",
"chatHistoryDefault": "Questa conversazione sarà cancellata quando Cwtch sarà chiuso! La cronologia dei messaggi può essere abilitata per ogni conversazione tramite il menu Impostazioni in alto a destra.",
"accepted": "Accettato!",
"rejected": "Rifiutato!",
"contactSuggestion": "Questo è un suggerimento di contatto per:",
"sendAnInvitation": "Hai inviato un invito per:",
"torVersion": "Versione di Tor",
"torStatus": "Stato di Tor",
"resetTor": "Resettare",
"cancel": "Annulla",
"sendMessage": "Invia messaggio",
"sendInvite": "Invia un invito a un contatto o a un gruppo",
"deleteProfileSuccess": "Profilo eliminato con successo",
"addServerFirst": "È necessario aggiungere un server prima di poter creare un gruppo",
"nickChangeSuccess": "Nickname del profilo modificato con successo",
"createProfileToBegin": "Crea o sblocca un profilo per iniziare",
"addContactFirst": "Aggiungi o scegli un contatto per iniziare a chattare.",
"torNetworkStatus": "Stato della rete Tor",
"debugLog": "Attiva la registrazione del debug della console",
"profileDeleteSuccess": "Profilo eliminato con successo",
"malformedMessage": "Messaggio non valido",
"shutdownCwtchTooltip": "Chiudi Cwtch",
"shutdownCwtchDialogTitle": "Chiudi Cwtch?",
"shutdownCwtchAction": "Chiudi Cwtch",
"shutdownCwtchDialog": "Sei sicuro di voler chiudere Cwtch? Questo chiuderà tutte le connessioni e uscirà dall'applicazione.",
"groupInviteSettingsWarning": "Sei stato invitato ad unirti ad un gruppo! Abilita l'Esperimento di chat di gruppo in Impostazioni per visualizzare questo Invito.",
"tooltipShowPassword": "Mostra la password",
"tooltipHidePassword": "Nascondi la password",
"notificationNewMessageFromPeer": "Nuovo messaggio da un contatto!",
"notificationNewMessageFromGroup": "Nuovo messaggio in un gruppo!",
"tooltipAcceptContactRequest": "Accetta questa richiesta di contatto.",
"tooltipRejectContactRequest": "Rifiuta questa richiesta di contatto",
"versionBuilddate": "Versione: %1 Costruito il: %2",
"versionTor": "Versione %1 con tor %2",
"version": "Versione %1",
"builddate": "Costruito il: %2",
"cycleMorphsAndroid": "Fare clic per scorrere i morph.\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Fare clic per scorrere i morph.\nCliccare con il tasto destro per resettare.",
"localeEn": "Inglese",
"localeIt": "Italiano", "localeIt": "Italiano",
"localeEs": "Spagnolo", "localeEs": "Spagnolo",
"addListItem": "Aggiungi un nuovo elemento alla lista",
"addNewItem": "Aggiungi un nuovo elemento alla lista",
"todoPlaceholder": "Da fare...",
"newConnectionPaneTitle": "Nuova connessione",
"networkStatusOnline": "Online",
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
"viewGroupMembershipTooltip": "Visualizza i membri del gruppo",
"loadingTor": "Caricamento di tor...",
"smallTextLabel": "Piccolo",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
"builddate": "Costruito il: %2",
"version": "Versione %1",
"versionTor": "Versione %1 con tor %2",
"themeDark": "Scuro",
"themeLight": "Chiaro",
"settingTheme": "Tema",
"largeTextLabel": "Grande",
"settingInterfaceZoom": "Livello di zoom",
"localeDe": "Tedesco", "localeDe": "Tedesco",
"localePt": "Portoghese", "localePt": "Portoghese",
"localeFr": "Francese", "localeFr": "Francese",
"localeEn": "Inglese", "addListItem": "Aggiungi un nuovo elemento alla lista",
"settingLanguage": "Lingua", "savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)", "unblockBtn": "Sblocca il peer",
"versionBuilddate": "Versione: %1 Costruito il: %2",
"cwtchSettingsTitle": "Impostazioni di Cwtch",
"unlock": "Sblocca",
"yourServers": "I tuoi server",
"yourProfiles": "I tuoi profili",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"password": "Password",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"addNewProfileBtn": "Aggiungi nuovo profilo",
"deleteConfirmText": "ELIMINA",
"deleteProfileConfirmBtn": "Elimina realmente il profilo",
"deleteConfirmLabel": "Digita ELIMINA per confermare",
"deleteProfileBtn": "Elimina profilo",
"passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata",
"passwordErrorMatch": "Le password non corrispondono",
"saveProfileBtn": "Salva il profilo",
"createProfileBtn": "Crea un profilo",
"passwordErrorEmpty": "La password non può essere vuota",
"password2Label": "Reinserire la password",
"password1Label": "Password",
"currentPasswordLabel": "Password corrente",
"yourDisplayName": "Il tuo nome visualizzato",
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
"radioNoPassword": "Non criptato (senza password)",
"radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiato negli Appunti", "copiedToClipboardNotification": "Copiato negli Appunti",
"copyBtn": "Copia", "createGroupTitle": "Crea un gruppo",
"editProfile": "Modifica profilo",
"newProfile": "Nuovo profilo",
"defaultProfileName": "Alice",
"profileName": "Nome visualizzato",
"editProfileTitle": "Modifica profilo",
"addProfileTitle": "Aggiungi nuovo profilo",
"deleteBtn": "Elimina",
"saveBtn": "Salva",
"displayNameLabel": "Nome visualizzato",
"addressLabel": "Indirizzo",
"puzzleGameBtn": "Gioco di puzzle",
"bulletinsBtn": "Bollettini",
"listsBtn": "Liste",
"chatBtn": "Chat",
"rejectGroupBtn": "Rifiuta",
"acceptGroupBtn": "Accetta",
"acceptGroupInviteLabel": "Vuoi accettare l'invito a",
"newGroupBtn": "Crea un nuovo gruppo",
"copiedClipboardNotification": "Copiato negli Appunti",
"pendingLabel": "In corso",
"acknowledgedLabel": "Riconosciuto",
"couldNotSendMsgError": "Impossibile inviare questo messaggio",
"dmTooltip": "Clicca per inviare un Messagio Diretto",
"membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.",
"addListItemBtn": "Aggiungi elemento",
"searchList": "Cerca nella lista",
"update": "Aggiornamento",
"inviteBtn": "Invitare",
"inviteToGroupLabel": "Invitare nel gruppo",
"groupNameLabel": "Nome del gruppo",
"viewServerInfo": "Informazioni sul server",
"serverSynced": "Sincronizzato",
"serverConnectivityDisconnected": "Server disconnesso",
"serverConnectivityConnected": "Server connesso",
"serverInfo": "Informazioni sul server",
"invitationLabel": "Invito",
"serverLabel": "Server", "serverLabel": "Server",
"search": "Ricerca...", "groupNameLabel": "Nome del gruppo",
"defaultGroupName": "Gruppo fantastico",
"createGroupBtn": "Crea",
"addPeerTab": "Aggiungi un peer",
"createGroupTab": "Crea un gruppo",
"joinGroupTab": "Unisciti a un gruppo",
"peerAddress": "Indirizzo",
"peerName": "Nome",
"groupName": "Nome del gruppo",
"server": "Server",
"invitation": "Invito",
"groupAddr": "Indirizzo",
"newBulletinLabel": "Nuovo bollettino",
"addPeer": "Aggiungi peer",
"createGroup": "Crea un gruppo",
"joinGroup": "Unisciti al gruppo",
"postNewBulletinLabel": "Pubblica un nuovo bollettino",
"titlePlaceholder": "titolo...",
"blocked": "Bloccato",
"cycleCatsAndroid": "Fare clic per scorrere le categorie.\nPressione lunga per resettare.",
"cycleCatsDesktop": "Fare clic per scorrere le categorie.\nCliccare con il tasto destro per resettare.",
"viewGroupMembershipTooltip": "Visualizza i membri del gruppo",
"cycleColoursDesktop": "Fare clic per scorrere i colori.\nCliccare con il tasto destro per resettare.", "cycleColoursDesktop": "Fare clic per scorrere i colori.\nCliccare con il tasto destro per resettare.",
"cycleColoursAndroid": "Fare clic per scorrere i colori.\nPressione lunga per resettare.", "cycleColoursAndroid": "Fare clic per scorrere i colori.\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Fare clic per scorrere i morph.\nCliccare con il tasto destro per resettare.", "search": "Ricerca...",
"cycleMorphsAndroid": "Fare clic per scorrere i morph.\nPressione lunga per resettare.", "peerBlockedMessage": "Il peer è bloccato",
"cycleCatsDesktop": "Fare clic per scorrere le categorie.\nCliccare con il tasto destro per resettare.", "peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
"cycleCatsAndroid": "Fare clic per scorrere le categorie.\nPressione lunga per resettare.", "copyBtn": "Copia",
"blocked": "Bloccato", "copiedClipboardNotification": "Copiato negli Appunti",
"titlePlaceholder": "titolo...", "newGroupBtn": "Crea un nuovo gruppo",
"postNewBulletinLabel": "Pubblica un nuovo bollettino", "profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
"newBulletinLabel": "Nuovo bollettino", "invitationLabel": "Invito",
"joinGroup": "Unisciti al gruppo", "serverInfo": "Informazioni sul server",
"createGroup": "Crea un gruppo", "serverConnectivityConnected": "Server connesso",
"groupAddr": "Indirizzo", "serverConnectivityDisconnected": "Server disconnesso",
"invitation": "Invito", "membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.",
"server": "Server", "dmTooltip": "Clicca per inviare un Messagio Diretto",
"groupName": "Nome del gruppo", "couldNotSendMsgError": "Impossibile inviare questo messaggio",
"peerName": "Nome", "acknowledgedLabel": "Riconosciuto",
"peerAddress": "Indirizzo", "pendingLabel": "In corso",
"joinGroupTab": "Unisciti a un gruppo", "acceptGroupInviteLabel": "Vuoi accettare l'invito a",
"createGroupTab": "Crea un gruppo", "acceptGroupBtn": "Accetta",
"createGroupBtn": "Crea", "rejectGroupBtn": "Rifiuta",
"defaultGroupName": "Gruppo fantastico", "chatBtn": "Chat",
"createGroupTitle": "Crea un gruppo" "listsBtn": "Liste",
"bulletinsBtn": "Bollettini",
"puzzleGameBtn": "Gioco di puzzle",
"addressLabel": "Indirizzo",
"displayNameLabel": "Nome visualizzato",
"saveBtn": "Salva",
"blockBtn": "Blocca il peer",
"savePeerHistory": "Salva cronologia peer",
"dontSavePeerHistory": "Elimina cronologia dei peer",
"password": "Password",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"yourProfiles": "I tuoi profili",
"yourServers": "I tuoi server",
"unlock": "Sblocca",
"newConnectionPaneTitle": "Nuova connessione",
"addNewItem": "Aggiungi un nuovo elemento alla lista",
"todoPlaceholder": "Da fare...",
"serverSynced": "Sincronizzato",
"viewServerInfo": "Informazioni sul server",
"inviteToGroupLabel": "Invitare nel gruppo",
"inviteBtn": "Invitare",
"deleteBtn": "Elimina",
"update": "Aggiornamento",
"searchList": "Cerca nella lista",
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"addListItemBtn": "Aggiungi elemento",
"addProfileTitle": "Aggiungi nuovo profilo",
"editProfileTitle": "Modifica profilo",
"profileName": "Nome visualizzato",
"defaultProfileName": "Alice",
"editProfile": "Modifica profilo",
"newProfile": "Nuovo profilo",
"radioUsePassword": "Password",
"radioNoPassword": "Non criptato (senza password)",
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
"yourDisplayName": "Il tuo nome visualizzato",
"currentPasswordLabel": "Password corrente",
"password1Label": "Password",
"password2Label": "Reinserire la password",
"passwordErrorEmpty": "La password non può essere vuota",
"createProfileBtn": "Crea un profilo",
"saveProfileBtn": "Salva il profilo",
"passwordErrorMatch": "Le password non corrispondono",
"passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata",
"deleteProfileBtn": "Elimina profilo",
"deleteConfirmLabel": "Digita ELIMINA per confermare",
"deleteProfileConfirmBtn": "Elimina realmente il profilo",
"deleteConfirmText": "ELIMINA",
"addNewProfileBtn": "Aggiungi nuovo profilo",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"cwtchSettingsTitle": "Impostazioni di Cwtch",
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)",
"blockUnknownLabel": "Blocca peer sconosciuti",
"settingLanguage": "Lingua",
"settingInterfaceZoom": "Livello di zoom",
"largeTextLabel": "Grande",
"settingTheme": "Tema",
"themeLight": "Chiaro",
"themeDark": "Scuro",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
"smallTextLabel": "Piccolo",
"loadingTor": "Caricamento di tor...",
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
"networkStatusOnline": "Online"
} }

View File

@ -1,124 +1,121 @@
{ {
"@@locale": "pl", "@@locale": "pl",
"@@last_modified": "2021-11-11T01:02:08+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"newMessagesLabel": "New Messages", "experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"localeRU": "Russian", "enableExperimentClickableLinks": "Enable Clickable Links",
"copyServerKeys": "Kopiuj klucze", "serverConnectionsLabel": "Connection",
"verfiyResumeButton": "Zweryfikuj\/wznów", "serverTotalMessagesLabel": "Total Messages",
"fileCheckingStatus": "Sprawdzanie stanu pobierania", "serverMetricsLabel": "Server Metrics",
"fileInterrupted": "Przerwane", "manageKnownServersShort": "Servers",
"fileSavedTo": "Zapisano do", "manageKnownServersLong": "Manage Known Servers",
"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.", "displayNameTooltip": "Please enter a display name",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.", "manageKnownServersButton": "Manage Known Servers",
"deleteServerConfirmBtn": "Naprawdę usuń serwer", "fieldDescriptionLabel": "Description",
"deleteServerSuccess": "Pomyślnie usunięto serwer", "groupsOnThisServerLabel": "Groups I am in hosted on this server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", "importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"titleManageServers": "Zarządzaj serwerami",
"leaveGroup": "Wyjdź z tej rozmowy",
"yesLeave": "Tak, wyjdź z tej rozmowy",
"newPassword": "Nowe hasło",
"accepted": "Przyjęte!",
"rejected": "Odrzucone!",
"sendAnInvitation": "You sent an invitation for: ",
"torVersion": "Wersja Tor",
"torStatus": "Status Tor",
"resetTor": "Reset",
"cancel": "Anuluj",
"sendMessage": "Wyślij wiadomość",
"sendInvite": "Wyślij kontakt lub zaproszenie do grupy",
"deleteProfileSuccess": "Pomyślnie usunięto profil",
"nickChangeSuccess": "Nick w profilu został zmieniony pomyślnie",
"torNetworkStatus": "Stan sieci Tor",
"debugLog": "Włącz logowanie debugowania konsoli",
"profileDeleteSuccess": "Pomyślnie usunięto profil",
"malformedMessage": "Źle sformatowana wiadomość",
"shutdownCwtchTooltip": "Zamknij Cwtch",
"shutdownCwtchDialogTitle": "Zamknąć Cwtch?",
"shutdownCwtchAction": "Zamknij Cwtch",
"tooltipShowPassword": "Pokaż hasło",
"tooltipHidePassword": "Ukryj hasło",
"notificationNewMessageFromPeer": "Nowa wiadomość od kontaktu!",
"notificationNewMessageFromGroup": "Nowa wiadomość w grupie!",
"tooltipAcceptContactRequest": "Zaakceptuj tę prośbę o kontakt.",
"tooltipRejectContactRequest": "Odrzuć tę prośbę o kontakt",
"tooltipReplyToThisMessage": "Odpowiedz na tę wiadomość",
"tooltipRemoveThisQuotedMessage": "Usuń cytowaną wiadomość.",
"settingUIColumnDouble12Ratio": "Podwójny (1:2)",
"settingUIColumnSingle": "Pojedynczy",
"settingUIColumnDouble14Ratio": "Podwójny (1:4)",
"settingUIColumnOptionSame": "Tak samo jak w przypadku trybu portretowego",
"contactGoto": "Przejdź do rozmowy z %1",
"addContact": "Dodaj kontakt",
"addContactConfirm": "Dodaj kontakt %1",
"placeholderEnterMessage": "Wpisz wiadomość...",
"blockedMessageMessage": "Ta wiadomość pochodzi z profilu, który został przez Ciebie zablokowany.",
"showMessageButton": "Pokaż wiadomość",
"addServerTitle": "Dodaj serwer",
"editServerTitle": "Edytuj serwer",
"serverAddress": "Adres serwera",
"serverDescriptionLabel": "Opis serwera",
"serverEnabled": "Serwer włączony",
"serverEnabledDescription": "Uruchom lub zatrzymaj serwer",
"serverAutostartLabel": "Autostart",
"saveServerButton": "Zapisz serwer",
"serversManagerTitleLong": "Serwery, które hostujesz",
"serversManagerTitleShort": "Serwery",
"addServerTooltip": "Dodaj nowy serwer",
"enterServerPassword": "Wprowadź hasło, aby odblokować serwer",
"settingServers": "Hosting serwerów",
"enterCurrentPasswordForDeleteServer": "Wprowadź aktualne hasło, aby usunąć ten serwer",
"newMessagesLabel": "Nowe wiadomości",
"localePl": "Polski",
"localeRU": "Rosyjski",
"copyAddress": "Skopiuj adres", "copyAddress": "Skopiuj adres",
"deleteServerSuccess": "Pomyślnie usunięto serwer",
"deleteServerConfirmBtn": "Naprawdę usuń serwer",
"fileSavedTo": "Zapisano do",
"fileCheckingStatus": "Sprawdzanie stanu pobierania",
"verfiyResumeButton": "Zweryfikuj\/wznów",
"copyServerKeys": "Kopiuj klucze",
"fileInterrupted": "Przerwane",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"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.",
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers", "settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
"settingServers": "Hosting Servers",
"enterServerPassword": "Enter password to unlock server",
"unlockProfileTip": "Please create or unlock a profile to begin!", "unlockProfileTip": "Please create or unlock a profile to begin!",
"unlockServerTip": "Please create or unlock a server to begin!", "unlockServerTip": "Please create or unlock a server to begin!",
"addServerTooltip": "Add new server",
"serversManagerTitleShort": "Servers",
"serversManagerTitleLong": "Servers You Host",
"saveServerButton": "Save Server",
"serverAutostartDescription": "Controls if the application will automatically launch the server on start", "serverAutostartDescription": "Controls if the application will automatically launch the server on start",
"serverAutostartLabel": "Autostart",
"serverEnabledDescription": "Start or stop the server",
"serverEnabled": "Server Enabled",
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared", "serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
"serverDescriptionLabel": "Server Description", "blockUnknownConnectionsEnabledDescription": "Połączenia od nieznanych kontaktów są blokowane. Można to zmienić w Ustawieniach",
"serverAddress": "Server Address", "archiveConversation": "Zarchiwizuj tę rozmowę",
"editServerTitle": "Edit Server", "streamerModeLabel": "Tryb streamera\/prezentacji",
"addServerTitle": "Add Server", "retrievingManifestMessage": "Pobieranie informacji o pliku...",
"openFolderButton": "Otwórz folder",
"downloadFileButton": "Pobierz",
"labelFilename": "Nazwa pliku",
"labelFilesize": "Rozmiar",
"messageEnableFileSharing": "Włącz eksperyment udostępniania plików, aby wyświetlić tę wiadomość.",
"messageFileSent": "Plik został wysłany",
"messageFileOffered": "Kontakt proponuje wysłanie Ci pliku",
"tooltipSendFile": "Wyślij plik",
"settingFileSharing": "Udostępnianie plików",
"descriptionFileSharing": "Eksperyment udostępniania plików pozwala na wysyłanie i odbieranie plików od kontaktów i grup Cwtch. Zauważ, że udostępnienie pliku grupie spowoduje, że członkowie tej grupy połączą się z Tobą bezpośrednio przez Cwtch, aby go pobrać.",
"titleManageProfilesShort": "Profile", "titleManageProfilesShort": "Profile",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses", "descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "Eksperyment udostępniania plików pozwala na wysyłanie i odbieranie plików od kontaktów i grup Cwtch. Zauważ, że udostępnienie pliku grupie spowoduje, że członkowie tej grupy połączą się z Tobą bezpośrednio przez Cwtch, aby go pobrać.",
"settingFileSharing": "Udostępnianie plików",
"tooltipSendFile": "Wyślij plik",
"messageFileOffered": "Kontakt proponuje wysłanie Ci pliku",
"messageFileSent": "Plik został wysłany",
"messageEnableFileSharing": "Włącz eksperyment udostępniania plików, aby wyświetlić tę wiadomość.",
"labelFilesize": "Rozmiar",
"labelFilename": "Nazwa pliku",
"downloadFileButton": "Pobierz",
"openFolderButton": "Otwórz folder",
"retrievingManifestMessage": "Pobieranie informacji o pliku...",
"streamerModeLabel": "Tryb streamera\/prezentacji",
"archiveConversation": "Zarchiwizuj tę rozmowę",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"addPeerTab": "Add a contact",
"addPeer": "Add Contact",
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
"peerBlockedMessage": "Contact is blocked",
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
"blockBtn": "Block Contact",
"savePeerHistory": "Save History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
"dontSavePeerHistory": "Delete History",
"unblockBtn": "Unblock Contact",
"blockUnknownLabel": "Block Unknown Contacts",
"blockUnknownConnectionsEnabledDescription": "Połączenia od nieznanych kontaktów są blokowane. Można to zmienić w Ustawieniach",
"networkStatusConnecting": "Connecting to network and contacts...",
"showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...",
"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.", "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.", "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": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)",
"settingUIColumnSingle": "Single",
"settingUIColumnLandscape": "UI Columns in Landscape Mode", "settingUIColumnLandscape": "UI Columns in Landscape Mode",
"settingUIColumnPortrait": "UI Columns in Portrait Mode", "settingUIColumnPortrait": "UI Columns in Portrait Mode",
"localePl": "Polish",
"tooltipRemoveThisQuotedMessage": "Remove quoted message.",
"tooltipReplyToThisMessage": "Reply to this message",
"tooltipRejectContactRequest": "Reject this contact request",
"tooltipAcceptContactRequest": "Accept this contact request.",
"notificationNewMessageFromGroup": "New message in a group!",
"notificationNewMessageFromPeer": "New message from a contact!",
"tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",
"shutdownCwtchTooltip": "Shutdown Cwtch",
"malformedMessage": "Malformed message",
"profileDeleteSuccess": "Successfully deleted profile",
"debugLog": "Turn on console debug logging",
"torNetworkStatus": "Tor network status",
"addContactFirst": "Add or pick a contact to begin chatting.", "addContactFirst": "Add or pick a contact to begin chatting.",
"createProfileToBegin": "Please create or unlock a profile to begin", "createProfileToBegin": "Please create or unlock a profile to begin",
"nickChangeSuccess": "Profile nickname changed successfully",
"addServerFirst": "You need to add a server before you can create a group", "addServerFirst": "You need to add a server before you can create a group",
"deleteProfileSuccess": "Successfully deleted profile",
"sendInvite": "Send a contact or group invite",
"sendMessage": "Send Message",
"cancel": "Cancel",
"resetTor": "Reset",
"torStatus": "Tor Status",
"torVersion": "Tor Version",
"sendAnInvitation": "You sent an invitation for: ",
"contactSuggestion": "This is a contact suggestion for: ", "contactSuggestion": "This is a contact suggestion for: ",
"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.", "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.", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:", "inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers",
"dateNever": "Never", "dateNever": "Never",
"dateLastYear": "Last Year", "dateLastYear": "Last Year",
"dateYesterday": "Yesterday", "dateYesterday": "Yesterday",
@ -130,20 +127,22 @@
"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.", "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", "titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"titleManageContacts": "Conversations",
"tooltipAddContact": "Add a new contact or conversation",
"tooltipOpenSettings": "Open the settings pane", "tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists", "contactAlreadyExists": "Contact Already Exists",
"invalidImportString": "Invalid import string",
"conversationSettings": "Conversation Settings", "conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat", "enableGroups": "Enable Group Chat",
"experimentsEnabled": "Enable Experiments",
"localeIt": "Italiana", "localeIt": "Italiana",
"localeEs": "Espanol", "localeEs": "Espanol",
"addListItem": "Add a New List Item",
"addNewItem": "Add a new item to the list",
"todoPlaceholder": "Todo...", "todoPlaceholder": "Todo...",
"addNewItem": "Add a new item to the list",
"addListItem": "Add a New List Item",
"newConnectionPaneTitle": "New Connection", "newConnectionPaneTitle": "New Connection",
"networkStatusOnline": "Online", "networkStatusOnline": "Online",
"networkStatusConnecting": "Connecting to network and contacts...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network", "networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection", "networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership", "viewGroupMembershipTooltip": "View Group Membership",
@ -153,6 +152,7 @@
"builddate": "Built on: %2", "builddate": "Built on: %2",
"version": "Version %1", "version": "Version %1",
"versionTor": "Version %1 with tor %2", "versionTor": "Version %1 with tor %2",
"experimentsEnabled": "Enable Experiments",
"themeDark": "Dark", "themeDark": "Dark",
"themeLight": "Light", "themeLight": "Light",
"settingTheme": "Theme", "settingTheme": "Theme",
@ -163,6 +163,7 @@
"localeFr": "Frances", "localeFr": "Frances",
"localeEn": "English", "localeEn": "English",
"settingLanguage": "Language", "settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Contacts",
"zoomLabel": "Interface zoom (mostly affects text and button sizes)", "zoomLabel": "Interface zoom (mostly affects text and button sizes)",
"versionBuilddate": "Version: %1 Built on: %2", "versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Cwtch Settings", "cwtchSettingsTitle": "Cwtch Settings",
@ -186,11 +187,11 @@
"password1Label": "Password", "password1Label": "Password",
"currentPasswordLabel": "Current Password", "currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name", "yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)", "radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password", "radioUsePassword": "Password",
"copiedToClipboardNotification": "Copied to Clipboard", "copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"editProfile": "Edit Profille", "editProfile": "Edit Profille",
"newProfile": "New Profile", "newProfile": "New Profile",
"defaultProfileName": "Alice", "defaultProfileName": "Alice",
@ -198,6 +199,11 @@
"editProfileTitle": "Edit Profile", "editProfileTitle": "Edit Profile",
"addProfileTitle": "Add new profile", "addProfileTitle": "Add new profile",
"deleteBtn": "Delete", "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", "saveBtn": "Save",
"displayNameLabel": "Display Name", "displayNameLabel": "Display Name",
"addressLabel": "Address", "addressLabel": "Address",
@ -210,18 +216,23 @@
"acceptGroupInviteLabel": "Do you want to accept the invitation to", "acceptGroupInviteLabel": "Do you want to accept the invitation to",
"newGroupBtn": "Create new group", "newGroupBtn": "Create new group",
"copiedClipboardNotification": "Copied to clipboard", "copiedClipboardNotification": "Copied to clipboard",
"copyBtn": "Copy",
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
"peerBlockedMessage": "Contact is blocked",
"pendingLabel": "Pending", "pendingLabel": "Pending",
"acknowledgedLabel": "Acknowledged", "acknowledgedLabel": "Acknowledged",
"couldNotSendMsgError": "Could not send this message", "couldNotSendMsgError": "Could not send this message",
"dmTooltip": "Click to DM", "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.", "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", "addListItemBtn": "Add Item",
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
"searchList": "Search List", "searchList": "Search List",
"update": "Update", "update": "Update",
"inviteBtn": "Invite", "inviteBtn": "Invite",
"inviteToGroupLabel": "Invite to group", "inviteToGroupLabel": "Invite to group",
"groupNameLabel": "Group name", "groupNameLabel": "Group name",
"viewServerInfo": "Server Info", "viewServerInfo": "Server Info",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"serverSynced": "Synced", "serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected", "serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected", "serverConnectivityConnected": "Server Connected",
@ -236,11 +247,13 @@
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked", "blocked": "Blocked",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation",
"titlePlaceholder": "title...", "titlePlaceholder": "title...",
"postNewBulletinLabel": "Post new bulletin", "postNewBulletinLabel": "Post new bulletin",
"newBulletinLabel": "New Bulletin", "newBulletinLabel": "New Bulletin",
"joinGroup": "Join group", "joinGroup": "Join group",
"createGroup": "Create group", "createGroup": "Create group",
"addPeer": "Add Contact",
"groupAddr": "Address", "groupAddr": "Address",
"invitation": "Invitation", "invitation": "Invitation",
"server": "Server", "server": "Server",
@ -249,6 +262,7 @@
"peerAddress": "Address", "peerAddress": "Address",
"joinGroupTab": "Join a group", "joinGroupTab": "Join a group",
"createGroupTab": "Create a group", "createGroupTab": "Create a group",
"addPeerTab": "Add a contact",
"createGroupBtn": "Create", "createGroupBtn": "Create",
"defaultGroupName": "Awesome Group", "defaultGroupName": "Awesome Group",
"createGroupTitle": "Create Group" "createGroupTitle": "Create Group"

View File

@ -1,6 +1,20 @@
{ {
"@@locale": "pt", "@@locale": "pt",
"@@last_modified": "2021-11-11T01:02:08+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"newMessagesLabel": "New Messages", "newMessagesLabel": "New Messages",
"localeRU": "Russian", "localeRU": "Russian",
"copyServerKeys": "Copy keys", "copyServerKeys": "Copy keys",
@ -8,8 +22,8 @@
"fileCheckingStatus": "Checking download status", "fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted", "fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to", "fileSavedTo": "Saved to",
"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.",
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.", "encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
"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": "Really delete server", "deleteServerConfirmBtn": "Really delete server",
"deleteServerSuccess": "Successfully deleted server", "deleteServerSuccess": "Successfully deleted server",
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
@ -33,7 +47,6 @@
"editServerTitle": "Edit Server", "editServerTitle": "Edit Server",
"addServerTitle": "Add Server", "addServerTitle": "Add Server",
"titleManageProfilesShort": "Profiles", "titleManageProfilesShort": "Profiles",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.", "descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
"settingFileSharing": "File Sharing", "settingFileSharing": "File Sharing",
"tooltipSendFile": "Send File", "tooltipSendFile": "Send File",
@ -45,22 +58,10 @@
"downloadFileButton": "Download", "downloadFileButton": "Download",
"openFolderButton": "Open Folder", "openFolderButton": "Open Folder",
"retrievingManifestMessage": "Retrieving file information...", "retrievingManifestMessage": "Retrieving file information...",
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
"streamerModeLabel": "Streamer\/Presentation Mode", "streamerModeLabel": "Streamer\/Presentation Mode",
"archiveConversation": "Archive this Conversation", "archiveConversation": "Archive this Conversation",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"addPeerTab": "Add a contact",
"addPeer": "Add Contact",
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
"peerBlockedMessage": "Contact is blocked",
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
"blockBtn": "Block Contact",
"savePeerHistory": "Save History",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
"dontSavePeerHistory": "Delete History",
"unblockBtn": "Unblock Contact",
"blockUnknownLabel": "Block Unknown Contacts",
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings", "blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
"networkStatusConnecting": "Connecting to network and contacts...",
"showMessageButton": "Show Message", "showMessageButton": "Show Message",
"blockedMessageMessage": "This message is from a profile you have blocked.", "blockedMessageMessage": "This message is from a profile you have blocked.",
"placeholderEnterMessage": "Type a message...", "placeholderEnterMessage": "Type a message...",
@ -84,7 +85,6 @@
"notificationNewMessageFromPeer": "New message from a contact!", "notificationNewMessageFromPeer": "New message from a contact!",
"tooltipHidePassword": "Hide Password", "tooltipHidePassword": "Hide Password",
"tooltipShowPassword": "Show Password", "tooltipShowPassword": "Show Password",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch", "shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
@ -115,9 +115,6 @@
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation", "leaveGroup": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:", "inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
"tooltipAddContact": "Add a new contact or conversation",
"titleManageContacts": "Conversations",
"titleManageServers": "Manage Servers", "titleManageServers": "Manage Servers",
"dateNever": "Never", "dateNever": "Never",
"dateLastYear": "Last Year", "dateLastYear": "Last Year",
@ -130,20 +127,22 @@
"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.", "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", "titleManageProfiles": "Manage Cwtch Profiles",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"titleManageContacts": "Conversations",
"tooltipAddContact": "Add a new contact or conversation",
"tooltipOpenSettings": "Open the settings pane", "tooltipOpenSettings": "Open the settings pane",
"invalidImportString": "Invalid import string",
"contactAlreadyExists": "Contact Already Exists", "contactAlreadyExists": "Contact Already Exists",
"invalidImportString": "Invalid import string",
"conversationSettings": "Conversation Settings", "conversationSettings": "Conversation Settings",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"enableGroups": "Enable Group Chat", "enableGroups": "Enable Group Chat",
"experimentsEnabled": "Enable Experiments",
"localeIt": "Italiana", "localeIt": "Italiana",
"localeEs": "Espanol", "localeEs": "Espanol",
"addListItem": "Adicionar Item à Lista",
"addNewItem": "Adicionar novo item à lista",
"todoPlaceholder": "Afazer…", "todoPlaceholder": "Afazer…",
"addNewItem": "Adicionar novo item à lista",
"addListItem": "Adicionar Item à Lista",
"newConnectionPaneTitle": "New Connection", "newConnectionPaneTitle": "New Connection",
"networkStatusOnline": "Online", "networkStatusOnline": "Online",
"networkStatusConnecting": "Connecting to network and contacts...",
"networkStatusAttemptingTor": "Attempting to connect to Tor network", "networkStatusAttemptingTor": "Attempting to connect to Tor network",
"networkStatusDisconnected": "Disconnected from the internet, check your connection", "networkStatusDisconnected": "Disconnected from the internet, check your connection",
"viewGroupMembershipTooltip": "View Group Membership", "viewGroupMembershipTooltip": "View Group Membership",
@ -153,6 +152,7 @@
"builddate": "Built on: %2", "builddate": "Built on: %2",
"version": "Version %1", "version": "Version %1",
"versionTor": "Version %1 with tor %2", "versionTor": "Version %1 with tor %2",
"experimentsEnabled": "Enable Experiments",
"themeDark": "Dark", "themeDark": "Dark",
"themeLight": "Light", "themeLight": "Light",
"settingTheme": "Theme", "settingTheme": "Theme",
@ -163,6 +163,7 @@
"localeFr": "Frances", "localeFr": "Frances",
"localeEn": "English", "localeEn": "English",
"settingLanguage": "Language", "settingLanguage": "Language",
"blockUnknownLabel": "Block Unknown Contacts",
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)", "zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)",
"versionBuilddate": "Version: %1 Built on: %2", "versionBuilddate": "Version: %1 Built on: %2",
"cwtchSettingsTitle": "Configurações do Cwtch", "cwtchSettingsTitle": "Configurações do Cwtch",
@ -186,11 +187,11 @@
"password1Label": "Password", "password1Label": "Password",
"currentPasswordLabel": "Current Password", "currentPasswordLabel": "Current Password",
"yourDisplayName": "Your Display Name", "yourDisplayName": "Your Display Name",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"radioNoPassword": "Unencrypted (No password)", "radioNoPassword": "Unencrypted (No password)",
"radioUsePassword": "Password", "radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiado", "copiedToClipboardNotification": "Copiado",
"copyBtn": "Copiar",
"editProfile": "Edit Profille", "editProfile": "Edit Profille",
"newProfile": "New Profile", "newProfile": "New Profile",
"defaultProfileName": "Alice", "defaultProfileName": "Alice",
@ -198,6 +199,11 @@
"editProfileTitle": "Edit Profile", "editProfileTitle": "Edit Profile",
"addProfileTitle": "Add new profile", "addProfileTitle": "Add new profile",
"deleteBtn": "Deletar", "deleteBtn": "Deletar",
"unblockBtn": "Unblock Contact",
"dontSavePeerHistory": "Delete History",
"savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.",
"savePeerHistory": "Save History",
"blockBtn": "Block Contact",
"saveBtn": "Salvar", "saveBtn": "Salvar",
"displayNameLabel": "Nome de Exibição", "displayNameLabel": "Nome de Exibição",
"addressLabel": "Endereço", "addressLabel": "Endereço",
@ -210,18 +216,23 @@
"acceptGroupInviteLabel": "Você quer aceitar o convite para", "acceptGroupInviteLabel": "Você quer aceitar o convite para",
"newGroupBtn": "Criar novo grupo", "newGroupBtn": "Criar novo grupo",
"copiedClipboardNotification": "Copiado", "copiedClipboardNotification": "Copiado",
"copyBtn": "Copiar",
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
"peerBlockedMessage": "Contact is blocked",
"pendingLabel": "Pendente", "pendingLabel": "Pendente",
"acknowledgedLabel": "Confirmada", "acknowledgedLabel": "Confirmada",
"couldNotSendMsgError": "Não deu para enviar esta mensagem", "couldNotSendMsgError": "Não deu para enviar esta mensagem",
"dmTooltip": "Clique para DM", "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.", "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": "Add Item", "addListItemBtn": "Add Item",
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
"searchList": "Search List", "searchList": "Search List",
"update": "Update", "update": "Update",
"inviteBtn": "Convidar", "inviteBtn": "Convidar",
"inviteToGroupLabel": "Convidar ao grupo", "inviteToGroupLabel": "Convidar ao grupo",
"groupNameLabel": "Nome do grupo", "groupNameLabel": "Nome do grupo",
"viewServerInfo": "Server Info", "viewServerInfo": "Server Info",
"serverNotSynced": "Syncing New Messages (This can take some time)...",
"serverSynced": "Synced", "serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected", "serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected", "serverConnectivityConnected": "Server Connected",
@ -236,11 +247,13 @@
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked", "blocked": "Blocked",
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
"titlePlaceholder": "título…", "titlePlaceholder": "título…",
"postNewBulletinLabel": "Postar novo boletim", "postNewBulletinLabel": "Postar novo boletim",
"newBulletinLabel": "Novo Boletim", "newBulletinLabel": "Novo Boletim",
"joinGroup": "Join group", "joinGroup": "Join group",
"createGroup": "Create group", "createGroup": "Create group",
"addPeer": "Add Contact",
"groupAddr": "Address", "groupAddr": "Address",
"invitation": "Invitation", "invitation": "Invitation",
"server": "Server", "server": "Server",
@ -249,6 +262,7 @@
"peerAddress": "Address", "peerAddress": "Address",
"joinGroupTab": "Join a group", "joinGroupTab": "Join a group",
"createGroupTab": "Create a group", "createGroupTab": "Create a group",
"addPeerTab": "Add a contact",
"createGroupBtn": "Criar", "createGroupBtn": "Criar",
"defaultGroupName": "Grupo incrível", "defaultGroupName": "Grupo incrível",
"createGroupTitle": "Criar Grupo" "createGroupTitle": "Criar Grupo"

View File

@ -1,14 +1,39 @@
{ {
"@@locale": "ru", "@@locale": "ru",
"@@last_modified": "2021-11-10T18:47:30+01:00", "@@last_modified": "2021-12-13T23:43:26+01:00",
"experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages",
"enableExperimentClickableLinks": "Enable Clickable Links",
"serverConnectionsLabel": "Connection",
"serverTotalMessagesLabel": "Total Messages",
"serverMetricsLabel": "Server Metrics",
"saveBtn": "Сохранить",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"manageKnownServersShort": "Servers",
"manageKnownServersLong": "Manage Known Servers",
"displayNameTooltip": "Please enter a display name",
"manageKnownServersButton": "Manage Known Servers",
"fieldDescriptionLabel": "Description",
"groupsOnThisServerLabel": "Groups I am in hosted on this server",
"importLocalServerButton": "Import %1",
"importLocalServerSelectText": "Select Local Server",
"importLocalServerLabel": "Import a locally hosted server",
"newMessagesLabel": "New Messages",
"networkStatusOnline": "В сети",
"copiedToClipboardNotification": "Скопировано в буфер обмена",
"defaultProfileName": "Алиса",
"deleteBtn": "Удалить",
"bulletinsBtn": "Бюллетень",
"groupNameLabel": "Имя группы",
"serverLabel": "Сервер",
"copyBtn": "Копировать",
"localeRU": "Russian", "localeRU": "Russian",
"copyServerKeys": "Копировать ключи", "copyServerKeys": "Копировать ключи",
"verfiyResumeButton": "Проверить\/продолжить", "verfiyResumeButton": "Проверить\/продолжить",
"fileCheckingStatus": "Проверка статуса загрузки", "fileCheckingStatus": "Проверка статуса загрузки",
"fileInterrupted": "Прервано", "fileInterrupted": "Прервано",
"fileSavedTo": "Сохранить в", "fileSavedTo": "Сохранить в",
"plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.",
"encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.", "encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
"plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.",
"deleteServerConfirmBtn": "Точно удалить сервер?", "deleteServerConfirmBtn": "Точно удалить сервер?",
"deleteServerSuccess": "Сервер успешно удален", "deleteServerSuccess": "Сервер успешно удален",
"enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его", "enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его",
@ -32,7 +57,6 @@
"editServerTitle": "Изменить сервер", "editServerTitle": "Изменить сервер",
"addServerTitle": "Добавить сервер", "addServerTitle": "Добавить сервер",
"titleManageProfilesShort": "Профили", "titleManageProfilesShort": "Профили",
"descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.", "descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.",
"settingFileSharing": "Передача файлов", "settingFileSharing": "Передача файлов",
"tooltipSendFile": "Отправить файл", "tooltipSendFile": "Отправить файл",
@ -44,22 +68,10 @@
"downloadFileButton": "Загрузить", "downloadFileButton": "Загрузить",
"openFolderButton": "Открыть папку", "openFolderButton": "Открыть папку",
"retrievingManifestMessage": "Получение информации о файле...", "retrievingManifestMessage": "Получение информации о файле...",
"descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена",
"streamerModeLabel": "Режим презентации", "streamerModeLabel": "Режим презентации",
"archiveConversation": "Отправить чат в архив", "archiveConversation": "Отправить чат в архив",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"addPeerTab": "Добавить контакт",
"addPeer": "Добавить контакт",
"peerNotOnline": "Контакт не в сети. Вы не можете связаться с ним пока он не появиться в сети.",
"peerBlockedMessage": "Контакт заблокирован",
"peerOfflineMessage": "Контакт не в сети, сообщения не могут быть отправлены",
"blockBtn": "Заблокировать контакт",
"savePeerHistory": "Хранить исторую",
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
"dontSavePeerHistory": "Удалить историю",
"unblockBtn": "Разблокировать контакт",
"blockUnknownLabel": "Блокировать неизвестные контакты",
"blockUnknownConnectionsEnabledDescription": "Соединения от неизвестных контактов блокируются. Данный параметр можно изменить в настройках", "blockUnknownConnectionsEnabledDescription": "Соединения от неизвестных контактов блокируются. Данный параметр можно изменить в настройках",
"networkStatusConnecting": "Подключение к сети и контактам...",
"showMessageButton": "Показать сообщения", "showMessageButton": "Показать сообщения",
"blockedMessageMessage": "Это сообщение из заблокированного вами профиля.", "blockedMessageMessage": "Это сообщение из заблокированного вами профиля.",
"placeholderEnterMessage": "Написать сообщение...", "placeholderEnterMessage": "Написать сообщение...",
@ -83,7 +95,6 @@
"notificationNewMessageFromPeer": "Новое сообщение от контакта!", "notificationNewMessageFromPeer": "Новое сообщение от контакта!",
"tooltipHidePassword": "Скрыть пароль", "tooltipHidePassword": "Скрыть пароль",
"tooltipShowPassword": "Показать пароль", "tooltipShowPassword": "Показать пароль",
"serverNotSynced": "Синхронизация новых сообщений (это может занять некоторое время)...",
"groupInviteSettingsWarning": "Вас пригласили присоединиться к группе! Пожалуйста, включите экспериментальную функцию групповые чаты в Настройках, чтобы просмотреть это приглашение.", "groupInviteSettingsWarning": "Вас пригласили присоединиться к группе! Пожалуйста, включите экспериментальную функцию групповые чаты в Настройках, чтобы просмотреть это приглашение.",
"shutdownCwtchAction": "Выключить Cwtch", "shutdownCwtchAction": "Выключить Cwtch",
"shutdownCwtchDialog": "Вы уверены, что хотите выключить Cwtch? Это приведет к закрытию всех подключений и выходу из приложения.", "shutdownCwtchDialog": "Вы уверены, что хотите выключить Cwtch? Это приведет к закрытию всех подключений и выходу из приложения.",
@ -114,9 +125,6 @@
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.", "reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
"leaveGroup": "Да, оставить этот чат", "leaveGroup": "Да, оставить этот чат",
"inviteToGroup": "Вас пригласили присоединиться к группе:", "inviteToGroup": "Вас пригласили присоединиться к группе:",
"pasteAddressToAddContact": "Вставьте адрес cwtch, приглашение или пакет ключей здесь, чтобы добавить их в контакты",
"tooltipAddContact": "Добавление нового контакта или разговора",
"titleManageContacts": "Разговоры",
"titleManageServers": "Управление серверами", "titleManageServers": "Управление серверами",
"dateNever": "Никогда", "dateNever": "Никогда",
"dateLastYear": "Прошлый год", "dateLastYear": "Прошлый год",
@ -129,20 +137,21 @@
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный част 1 на 1..", "descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный част 1 на 1..",
"titleManageProfiles": "Управление профилями Cwtch", "titleManageProfiles": "Управление профилями Cwtch",
"tooltipUnlockProfiles": "Разблокировать зашифрованные профили, введя их пароль.", "tooltipUnlockProfiles": "Разблокировать зашифрованные профили, введя их пароль.",
"titleManageContacts": "Разговоры",
"tooltipAddContact": "Добавление нового контакта или разговора",
"tooltipOpenSettings": "Откройте панель настроек", "tooltipOpenSettings": "Откройте панель настроек",
"invalidImportString": "Недействительная строка импорта",
"contactAlreadyExists": "Контакт уже существует", "contactAlreadyExists": "Контакт уже существует",
"invalidImportString": "Недействительная строка импорта",
"conversationSettings": "Настройки чата", "conversationSettings": "Настройки чата",
"enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.", "enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.",
"enableGroups": "Включить Групповые чаты", "enableGroups": "Включить Групповые чаты",
"experimentsEnabled": "Включить Экспериментальные функции",
"localeIt": "Итальянский", "localeIt": "Итальянский",
"localeEs": "Испанский", "localeEs": "Испанский",
"addListItem": "Добавить новый элемент",
"addNewItem": "Добавить новый элемент в список",
"todoPlaceholder": "Выполняю...", "todoPlaceholder": "Выполняю...",
"addNewItem": "Добавить новый элемент в список",
"addListItem": "Добавить новый элемент",
"newConnectionPaneTitle": "Новое соединение", "newConnectionPaneTitle": "Новое соединение",
"networkStatusOnline": "Online", "networkStatusConnecting": "Подключение к сети и контактам...",
"networkStatusAttemptingTor": "Попытка подключиться к сети Tor", "networkStatusAttemptingTor": "Попытка подключиться к сети Tor",
"networkStatusDisconnected": "Нет сети. Проверьте подключение к интернету", "networkStatusDisconnected": "Нет сети. Проверьте подключение к интернету",
"viewGroupMembershipTooltip": "Просмотр членства в группе", "viewGroupMembershipTooltip": "Просмотр членства в группе",
@ -152,6 +161,7 @@
"builddate": "Построен на: %2", "builddate": "Построен на: %2",
"version": "Версия %1", "version": "Версия %1",
"versionTor": "Версия %1 c tor %2", "versionTor": "Версия %1 c tor %2",
"experimentsEnabled": "Включить Экспериментальные функции",
"themeDark": "Темная", "themeDark": "Темная",
"themeLight": "Светлая", "themeLight": "Светлая",
"settingTheme": "Тема", "settingTheme": "Тема",
@ -162,6 +172,7 @@
"localeFr": "Французский", "localeFr": "Французский",
"localeEn": "Английский", "localeEn": "Английский",
"settingLanguage": "Язык", "settingLanguage": "Язык",
"blockUnknownLabel": "Блокировать неизвестные контакты",
"zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)", "zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)",
"versionBuilddate": "Версия: %1 Сборка от: %2", "versionBuilddate": "Версия: %1 Сборка от: %2",
"cwtchSettingsTitle": "Настройки Cwtch", "cwtchSettingsTitle": "Настройки Cwtch",
@ -188,20 +199,19 @@
"noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы", "noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы",
"radioNoPassword": "Незашифрованный (без пароля)", "radioNoPassword": "Незашифрованный (без пароля)",
"radioUsePassword": "Пароль", "radioUsePassword": "Пароль",
"copiedToClipboardNotification": "Copied to Clipboard",
"copyBtn": "Copy",
"editProfile": "Изменить профиль", "editProfile": "Изменить профиль",
"newProfile": "Новый профиль", "newProfile": "Новый профиль",
"defaultProfileName": "Alice",
"profileName": "Отображаемое имя", "profileName": "Отображаемое имя",
"editProfileTitle": "Изменить профиль", "editProfileTitle": "Изменить профиль",
"addProfileTitle": "Добавить новый профиль", "addProfileTitle": "Добавить новый профиль",
"deleteBtn": "Delete", "unblockBtn": "Разблокировать контакт",
"saveBtn": "Save", "dontSavePeerHistory": "Удалить историю",
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
"savePeerHistory": "Хранить исторую",
"blockBtn": "Заблокировать контакт",
"displayNameLabel": "Отображаемое имя", "displayNameLabel": "Отображаемое имя",
"addressLabel": "Адрес", "addressLabel": "Адрес",
"puzzleGameBtn": "Puzzle Game", "puzzleGameBtn": "Puzzle Game",
"bulletinsBtn": "Bulletins",
"listsBtn": "Списки", "listsBtn": "Списки",
"chatBtn": "Чат", "chatBtn": "Чат",
"rejectGroupBtn": "Отклонить", "rejectGroupBtn": "Отклонить",
@ -209,24 +219,26 @@
"acceptGroupInviteLabel": "Хотите принять приглашение в", "acceptGroupInviteLabel": "Хотите принять приглашение в",
"newGroupBtn": "Создать новую группу", "newGroupBtn": "Создать новую группу",
"copiedClipboardNotification": "Скопировано в буфер обмена", "copiedClipboardNotification": "Скопировано в буфер обмена",
"peerOfflineMessage": "Контакт не в сети, сообщения не могут быть отправлены",
"peerBlockedMessage": "Контакт заблокирован",
"pendingLabel": "Ожидаемый", "pendingLabel": "Ожидаемый",
"acknowledgedLabel": "Отправлено", "acknowledgedLabel": "Отправлено",
"couldNotSendMsgError": "Не удалось отправить это сообщение", "couldNotSendMsgError": "Не удалось отправить это сообщение",
"dmTooltip": "Нажмите, чтобы перейти в DM", "dmTooltip": "Нажмите, чтобы перейти в DM",
"membershipDescription": "Ниже приведен список пользователей, отправивших сообщения группе. Этот список может не отражать всех пользователей, имеющих доступ к группе.", "membershipDescription": "Ниже приведен список пользователей, отправивших сообщения группе. Этот список может не отражать всех пользователей, имеющих доступ к группе.",
"addListItemBtn": "Добавить элемент", "addListItemBtn": "Добавить элемент",
"peerNotOnline": "Контакт не в сети. Вы не можете связаться с ним пока он не появиться в сети.",
"searchList": "Список поиска", "searchList": "Список поиска",
"update": "Обновить", "update": "Обновить",
"inviteBtn": "Пригласить", "inviteBtn": "Пригласить",
"inviteToGroupLabel": "Пригласить в группу", "inviteToGroupLabel": "Пригласить в группу",
"groupNameLabel": "Group name",
"viewServerInfo": "Информация о сервере", "viewServerInfo": "Информация о сервере",
"serverNotSynced": "Синхронизация новых сообщений (это может занять некоторое время)...",
"serverSynced": "Синхронизировано", "serverSynced": "Синхронизировано",
"serverConnectivityDisconnected": "Сервер отключен", "serverConnectivityDisconnected": "Сервер отключен",
"serverConnectivityConnected": "Сервер подключен", "serverConnectivityConnected": "Сервер подключен",
"serverInfo": "Информация о сервере", "serverInfo": "Информация о сервере",
"invitationLabel": "Приглашение", "invitationLabel": "Приглашение",
"serverLabel": "Server",
"search": "Поиск...", "search": "Поиск...",
"cycleColoursDesktop": "Нажмите, чтобы переключать цвета.\nПравый клик чтобы сбросить.", "cycleColoursDesktop": "Нажмите, чтобы переключать цвета.\nПравый клик чтобы сбросить.",
"cycleColoursAndroid": "Нажмите, чтобы переключать цвета.\nНажмите и удерживайте, чтобы сбросить.", "cycleColoursAndroid": "Нажмите, чтобы переключать цвета.\nНажмите и удерживайте, чтобы сбросить.",
@ -235,11 +247,13 @@
"cycleCatsDesktop": "Нажмите, чтобы просмотреть категории.\nПравый клик чтобы сбросить.", "cycleCatsDesktop": "Нажмите, чтобы просмотреть категории.\nПравый клик чтобы сбросить.",
"cycleCatsAndroid": "Нажмите, чтобы просмотреть категории.\nНажмите и удерживайте, чтобы сбросить.", "cycleCatsAndroid": "Нажмите, чтобы просмотреть категории.\nНажмите и удерживайте, чтобы сбросить.",
"blocked": "Заблокировано", "blocked": "Заблокировано",
"pasteAddressToAddContact": "Вставьте адрес cwtch, приглашение или пакет ключей здесь, чтобы добавить их в контакты",
"titlePlaceholder": "заговолок...", "titlePlaceholder": "заговолок...",
"postNewBulletinLabel": "Опубликовать новый бюллетень", "postNewBulletinLabel": "Опубликовать новый бюллетень",
"newBulletinLabel": "Новый бюллетень", "newBulletinLabel": "Новый бюллетень",
"joinGroup": "Вступить в группу", "joinGroup": "Вступить в группу",
"createGroup": "Создать группу", "createGroup": "Создать группу",
"addPeer": "Добавить контакт",
"groupAddr": "Адрес", "groupAddr": "Адрес",
"invitation": "Приглашение", "invitation": "Приглашение",
"server": "Сервер", "server": "Сервер",
@ -248,6 +262,7 @@
"peerAddress": "Адрес", "peerAddress": "Адрес",
"joinGroupTab": "Присоединиться к группе", "joinGroupTab": "Присоединиться к группе",
"createGroupTab": "Создать группу", "createGroupTab": "Создать группу",
"addPeerTab": "Добавить контакт",
"createGroupBtn": "Создать", "createGroupBtn": "Создать",
"defaultGroupName": "Замечательная группа", "defaultGroupName": "Замечательная группа",
"createGroupTitle": "Создать группу" "createGroupTitle": "Создать группу"

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/profileservers.dart';
@ -29,7 +31,7 @@ class AppState extends ChangeNotifier {
bool cwtchIsClosing = false; bool cwtchIsClosing = false;
String appError = ""; String appError = "";
String? _selectedProfile; String? _selectedProfile;
String? _selectedConversation; int? _selectedConversation;
int _initialScrollIndex = 0; int _initialScrollIndex = 0;
int _hoveredIndex = -1; int _hoveredIndex = -1;
int? _selectedIndex; int? _selectedIndex;
@ -51,8 +53,8 @@ class AppState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
String? get selectedConversation => _selectedConversation; int? get selectedConversation => _selectedConversation;
set selectedConversation(String? newVal) { set selectedConversation(int? newVal) {
this._selectedConversation = newVal; this._selectedConversation = newVal;
notifyListeners(); notifyListeners();
} }
@ -118,6 +120,7 @@ class ProfileListState extends ChangeNotifier {
} }
class ContactListState extends ChangeNotifier { class ContactListState extends ChangeNotifier {
ProfileServerListState? servers;
List<ContactInfoState> _contacts = []; List<ContactInfoState> _contacts = [];
String _filter = ""; String _filter = "";
int get num => _contacts.length; int get num => _contacts.length;
@ -129,6 +132,10 @@ class ContactListState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void connectServers(ProfileServerListState servers) {
this.servers = servers;
}
List<ContactInfoState> filteredList() { 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(); return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList();
@ -136,11 +143,20 @@ class ContactListState extends ChangeNotifier {
void addAll(Iterable<ContactInfoState> newContacts) { void addAll(Iterable<ContactInfoState> newContacts) {
_contacts.addAll(newContacts); _contacts.addAll(newContacts);
servers?.clearGroups();
_contacts.forEach((contact) {
if (contact.isGroup) {
servers?.addGroup(contact);
}
});
notifyListeners(); notifyListeners();
} }
void add(ContactInfoState newContact) { void add(ContactInfoState newContact) {
_contacts.add(newContact); _contacts.add(newContact);
if (newContact.isGroup) {
servers?.addGroup(newContact);
}
notifyListeners(); notifyListeners();
} }
@ -172,8 +188,8 @@ class ContactListState extends ChangeNotifier {
//} </todo> //} </todo>
} }
void updateLastMessageTime(String forOnion, DateTime newMessageTime) { void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) {
var contact = getContact(forOnion); var contact = getContact(forIdentifier);
if (contact == null) return; if (contact == null) return;
// Assert that the new time is after the current last message time AND that // Assert that the new time is after the current last message time AND that
@ -191,23 +207,28 @@ class ContactListState extends ChangeNotifier {
List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ContactInfoState? getContact(String onion) { ContactInfoState? getContact(int identifier) {
int idx = _contacts.indexWhere((element) => element.onion == onion); int idx = _contacts.indexWhere((element) => element.identifier == identifier);
return idx >= 0 ? _contacts[idx] : null; return idx >= 0 ? _contacts[idx] : null;
} }
void removeContact(String onion) { void removeContact(int identifier) {
int idx = _contacts.indexWhere((element) => element.onion == onion); int idx = _contacts.indexWhere((element) => element.identifier == identifier);
if (idx >= 0) { if (idx >= 0) {
_contacts.removeAt(idx); _contacts.removeAt(idx);
notifyListeners(); notifyListeners();
} }
} }
ContactInfoState? findContact(String byHandle) {
int idx = _contacts.indexWhere((element) => element.onion == byHandle);
return idx >= 0 ? _contacts[idx] : null;
}
} }
class ProfileInfoState extends ChangeNotifier { class ProfileInfoState extends ChangeNotifier {
ContactListState _contacts = ContactListState();
ProfileServerListState _servers = ProfileServerListState(); ProfileServerListState _servers = ProfileServerListState();
ContactListState _contacts = ContactListState();
final String onion; final String onion;
String _nickname = ""; String _nickname = "";
String _imagePath = ""; String _imagePath = "";
@ -235,10 +256,14 @@ class ProfileInfoState extends ChangeNotifier {
this._online = online; this._online = online;
this._encrypted = encrypted; this._encrypted = encrypted;
_contacts.connectServers(this._servers);
if (contactsJson != null && contactsJson != "" && contactsJson != "null") { if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
this.replaceServers(serversJson);
List<dynamic> contacts = jsonDecode(contactsJson); List<dynamic> contacts = jsonDecode(contactsJson);
this._contacts.addAll(contacts.map((contact) { this._contacts.addAll(contacts.map((contact) {
return ContactInfoState(this.onion, contact["onion"], return ContactInfoState(this.onion, contact["identifier"], contact["onion"],
nickname: contact["name"], nickname: contact["name"],
status: contact["status"], status: contact["status"],
imagePath: contact["picture"], imagePath: contact["picture"],
@ -254,11 +279,11 @@ class ProfileInfoState extends ChangeNotifier {
// dummy set to invoke sort-on-load // dummy set to invoke sort-on-load
if (this._contacts.num > 0) { if (this._contacts.num > 0) {
this._contacts.updateLastMessageTime(this._contacts._contacts.first.onion, this._contacts._contacts.first.lastMessageTime); this._contacts.updateLastMessageTime(this._contacts._contacts.first.identifier, this._contacts._contacts.first.lastMessageTime);
} }
} }
this.replaceServers(serversJson);
} }
// Parse out the server list json into our server info state struct... // Parse out the server list json into our server info state struct...
@ -267,15 +292,22 @@ class ProfileInfoState extends ChangeNotifier {
List<dynamic> servers = jsonDecode(serversJson); List<dynamic> servers = jsonDecode(serversJson);
this._servers.replace(servers.map((server) { this._servers.replace(servers.map((server) {
// TODO Keys... // TODO Keys...
return RemoteServerInfoState(onion: server["onion"], status: server["status"]); return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]);
})); }));
this._contacts.contacts.forEach((contact) {
if (contact.isGroup) {
_servers.addGroup(contact);
}
});
notifyListeners(); notifyListeners();
} }
} }
// //
void updateServerStatusCache(String server, String status) { void updateServerStatusCache(String server, String status) {
this._servers.updateServerCache(server, status); this._servers.updateServerState(server, status);
notifyListeners(); notifyListeners();
} }
@ -341,6 +373,7 @@ class ProfileInfoState extends ChangeNotifier {
} else { } else {
this._contacts.add(ContactInfoState( this._contacts.add(ContactInfoState(
this.onion, this.onion,
contact["identifier"],
contact["onion"], contact["onion"],
nickname: contact["name"], nickname: contact["name"],
status: contact["status"], status: contact["status"],
@ -491,8 +524,15 @@ ContactAuthorization stringToContactAuthorization(String authStr) {
} }
} }
class MessageCache {
final MessageMetadata metadata;
final String wrapper;
MessageCache(this.metadata, this.wrapper);
}
class ContactInfoState extends ChangeNotifier { class ContactInfoState extends ChangeNotifier {
final String profileOnion; final String profileOnion;
final int identifier;
final String onion; final String onion;
late String _nickname; late String _nickname;
@ -504,6 +544,7 @@ class ContactInfoState extends ChangeNotifier {
late int _totalMessages = 0; late int _totalMessages = 0;
late DateTime _lastMessageTime; late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageRowState>> keys; late Map<String, GlobalKey<MessageRowState>> keys;
late List<MessageCache?> messageCache;
int _newMarker = 0; int _newMarker = 0;
DateTime _newMarkerClearAt = DateTime.now(); DateTime _newMarkerClearAt = DateTime.now();
@ -512,7 +553,7 @@ class ContactInfoState extends ChangeNotifier {
String? _server; String? _server;
late bool _archived; late bool _archived;
ContactInfoState(this.profileOnion, this.onion, ContactInfoState(this.profileOnion, this.identifier, this.onion,
{nickname = "", {nickname = "",
isGroup = false, isGroup = false,
authorization = ContactAuthorization.unknown, authorization = ContactAuthorization.unknown,
@ -535,6 +576,7 @@ class ContactInfoState extends ChangeNotifier {
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
this._server = server; this._server = server;
this._archived = archived; this._archived = archived;
this.messageCache = List.empty(growable: true);
keys = Map<String, GlobalKey<MessageRowState>>(); keys = Map<String, GlobalKey<MessageRowState>>();
} }
@ -590,7 +632,7 @@ class ContactInfoState extends ChangeNotifier {
if (newVal > 0) { if (newVal > 0) {
this._newMarker = newVal; this._newMarker = newVal;
} else { } else {
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes:2)); this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2));
} }
this._unreadMessages = newVal; this._unreadMessages = newVal;
notifyListeners(); notifyListeners();
@ -605,12 +647,13 @@ class ContactInfoState extends ChangeNotifier {
} }
return this._newMarker; return this._newMarker;
} }
// what's a getter that sometimes sets without a setter // what's a getter that sometimes sets without a setter
// that sometimes doesn't set // that sometimes doesn't set
set newMarker(int newVal) { set newMarker(int newVal) {
// only unreadMessages++ can set newMarker = 1; // only unreadMessages++ can set newMarker = 1;
// avoids drawing a marker when the convo is already open // avoids drawing a marker when the convo is already open
if (newVal > 1) { if (newVal >= 1) {
this._newMarker = newVal; this._newMarker = newVal;
notifyListeners(); notifyListeners();
} }
@ -646,11 +689,37 @@ class ContactInfoState extends ChangeNotifier {
} }
} }
GlobalKey<MessageRowState> getMessageKey(String index) { GlobalKey<MessageRowState> getMessageKey(int conversation, int message) {
String index = "c: " + conversation.toString() + " m:" + message.toString();
if (keys[index] == null) { if (keys[index] == null) {
keys[index] = GlobalKey<MessageRowState>(); keys[index] = GlobalKey<MessageRowState>();
} }
GlobalKey<MessageRowState> ret = keys[index]!; GlobalKey<MessageRowState> ret = keys[index]!;
return ret; return ret;
} }
GlobalKey<MessageRowState>? getMessageKeyOrFail(int conversation, int message) {
String index = "c: " + conversation.toString() + " m:" + message.toString();
if (keys[index] == null) {
return null;
}
GlobalKey<MessageRowState> ret = keys[index]!;
return ret;
}
void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data) {
this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false), data));
this.totalMessages += 1;
}
void bumpMessageCache() {
this.messageCache.insert(0, null);
this.totalMessages += 1;
}
void ackCache(int messageID) {
this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true;
notifyListeners();
}
} }

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/config.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -27,15 +28,54 @@ const GroupConversationHandleLength = 32;
abstract class Message { abstract class Message {
MessageMetadata getMetadata(); MessageMetadata getMetadata();
Widget getWidget(BuildContext context); Widget getWidget(BuildContext context, Key key);
Widget getPreviewWidget(BuildContext context); Widget getPreviewWidget(BuildContext context);
} }
Future<Message> messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) { Message compileOverlay(MessageMetadata metadata, String messageData) {
try { try {
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); dynamic message = jsonDecode(messageData);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
}
} catch (e) {
return MalformedMessage(metadata);
}
}
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) {
var cache = Provider.of<ProfileInfoState>(context).contactList.getContact(conversationIdentifier)?.messageCache;
if (cache != null && cache.length > index) {
if (cache[index] != null) {
return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper));
}
}
try {
Future<dynamic> rawMessageEnvelopeFuture;
if (byID) {
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, index);
} else {
rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index);
}
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
var metadata = MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true); var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", <String, String>{}, false, true);
try { try {
dynamic messageWrapper = jsonDecode(rawMessageEnvelope); dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
// There are 2 conditions in which this error condition can be met: // There are 2 conditions in which this error condition can be met:
@ -50,72 +90,48 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
return Future.delayed(Duration(seconds: 2), () { 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."); 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 messageHandler(context, profileOnion, contactHandle, index).then((value) => value); return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value);
}); });
} }
// Construct the initial metadata // Construct the initial metadata
var messageID = messageWrapper['ID'];
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
var senderHandle = messageWrapper['PeerID']; var senderHandle = messageWrapper['PeerID'];
var senderImage = messageWrapper['ContactImage']; var senderImage = messageWrapper['ContactImage'];
var flags = int.parse(messageWrapper['Flags'].toString()); var attributes = messageWrapper['Attributes'];
var ackd = messageWrapper['Acknowledged']; var ackd = messageWrapper['Acknowledged'];
var error = messageWrapper['Error'] != null; var error = messageWrapper['Error'] != null;
String? signature; var signature = messageWrapper['Signature'];
// If this is a group, store the signature metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error);
if (contactHandle.length == GroupConversationHandleLength) {
signature = messageWrapper['Signature'];
}
metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);
dynamic message = jsonDecode(messageWrapper['Message']); return compileOverlay(metadata, messageWrapper['Message']);
var content = message['d'] as dynamic;
var overlay = int.parse(message['o'].toString());
switch (overlay) {
case TextMessageOverlay:
return TextMessage(metadata, content);
case SuggestContactOverlay:
case InviteGroupOverlay:
return InviteMessage(overlay, metadata, content);
case QuotedMessageOverlay:
return QuotedMessage(metadata, content);
case FileShareOverlay:
return FileMessage(metadata, content);
default:
// Metadata is valid, content is not..
return MalformedMessage(metadata);
}
} catch (e) { } catch (e) {
print("an error! " + e.toString()); EnvironmentConfig.debugLog("an error! " + e.toString());
return MalformedMessage(metadata); return MalformedMessage(metadata);
} }
}); });
} catch (e) { } catch (e) {
return Future.value(MalformedMessage(MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true))); return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", <String, String>{}, false, true)));
} }
} }
class MessageMetadata extends ChangeNotifier { class MessageMetadata extends ChangeNotifier {
// meta-metadata // meta-metadata
final String profileOnion; final String profileOnion;
final String contactHandle; final int conversationIdentifier;
final int messageIndex; final int messageID;
final DateTime timestamp; final DateTime timestamp;
final String senderHandle; final String senderHandle;
final String? senderImage; final String? senderImage;
int _flags; final dynamic _attributes;
bool _ackd; bool _ackd;
bool _error; bool _error;
final String? signature; final String? signature;
int get flags => this._flags; dynamic get attributes => this._attributes;
set flags(int newVal) {
this._flags = newVal;
notifyListeners();
}
bool get ackd => this._ackd; bool get ackd => this._ackd;
set ackd(bool newVal) { set ackd(bool newVal) {
@ -129,5 +145,5 @@ class MessageMetadata extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
MessageMetadata(this.profileOnion, this.contactHandle, this.messageIndex, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error);
} }

View File

@ -17,11 +17,11 @@ class FileMessage extends Message {
FileMessage(this.metadata, this.content); FileMessage(this.metadata, this.content);
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
key: key,
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
dynamic shareObj = jsonDecode(this.content); dynamic shareObj = jsonDecode(this.content);
if (shareObj == null) { if (shareObj == null) {
return MessageRow(MalformedBubble()); return MessageRow(MalformedBubble());
@ -35,7 +35,7 @@ class FileMessage extends Message {
return MessageRow(MalformedBubble()); return MessageRow(MalformedBubble());
} }
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: key);
}); });
} }

View File

@ -17,18 +17,18 @@ class InviteMessage extends Message {
InviteMessage(this.overlay, this.metadata, this.content); InviteMessage(this.overlay, this.metadata, this.content);
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
key: key,
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
String inviteTarget; String inviteTarget;
String inviteNick; String inviteNick;
String invite = this.content; String invite = this.content;
if (this.content.length == TorV3ContactHandleLength) { if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content; inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget); var targetContact = Provider.of<ProfileInfoState>(context).contactList.findContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname; inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else { } else {
var parts = this.content.toString().split("||"); var parts = this.content.toString().split("||");
@ -37,10 +37,10 @@ class InviteMessage extends Message {
inviteTarget = jsonObj['GroupID']; inviteTarget = jsonObj['GroupID'];
inviteNick = jsonObj['GroupName']; inviteNick = jsonObj['GroupName'];
} else { } else {
return MessageRow(MalformedBubble()); return MessageRow(MalformedBubble(), key: key);
} }
} }
return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: key);
}); });
} }
@ -54,7 +54,7 @@ class InviteMessage extends Message {
String invite = this.content; String invite = this.content;
if (this.content.length == TorV3ContactHandleLength) { if (this.content.length == TorV3ContactHandleLength) {
inviteTarget = this.content; inviteTarget = this.content;
var targetContact = Provider.of<ProfileInfoState>(context).contactList.getContact(inviteTarget); var targetContact = Provider.of<ProfileInfoState>(context).contactList.findContact(inviteTarget);
inviteNick = targetContact == null ? this.content : targetContact.nickname; inviteNick = targetContact == null ? this.content : targetContact.nickname;
} else { } else {
var parts = this.content.toString().split("||"); var parts = this.content.toString().split("||");

View File

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

View File

@ -51,7 +51,7 @@ class QuotedMessage extends Message {
dynamic message = jsonDecode(this.content); dynamic message = jsonDecode(this.content);
return Text(message["body"]); return Text(message["body"]);
} catch (e) { } catch (e) {
return MalformedMessage(this.metadata).getWidget(context); return MalformedMessage(this.metadata).getWidget(context, Key("malformed"));
} }
}); });
} }
@ -62,16 +62,15 @@ class QuotedMessage extends Message {
} }
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
try { try {
dynamic message = jsonDecode(this.content); dynamic message = jsonDecode(this.content);
if (message["body"] == null || message["quotedHash"] == null) { if (message["body"] == null || message["quotedHash"] == null) {
return MalformedMessage(this.metadata).getWidget(context); return MalformedMessage(this.metadata).getWidget(context, key);
} }
var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]); var quotedMessagePotentials = Provider.of<FlwtchState>(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]);
int messageIndex = metadata.messageIndex;
Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) { Future<LocallyIndexedMessage?> quotedMessage = quotedMessagePotentials.then((matchingMessages) {
if (matchingMessages == "[]") { if (matchingMessages == "[]") {
return null; return null;
@ -81,9 +80,7 @@ class QuotedMessage extends Message {
// message // message
try { try {
var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); var list = (jsonDecode(matchingMessages) as List<dynamic>).map((data) => LocallyIndexedMessage.fromJson(data)).toList();
LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () { LocallyIndexedMessage candidate = list.reversed.first;
return list.firstWhere((element) => messageIndex > element.index);
});
return candidate; return candidate;
} catch (e) { } catch (e) {
// Malformed Message will be returned... // Malformed Message will be returned...
@ -94,18 +91,17 @@ class QuotedMessage extends Message {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
return MessageRow( return MessageRow(
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
if (localIndex != null) { if (localIndex != null) {
return messageHandler(context, metadata.profileOnion, metadata.contactHandle, localIndex.index); return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index);
} }
return MalformedMessage(this.metadata); return MalformedMessage(this.metadata);
})), })),
key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); key: key);
}); });
} catch (e) { } catch (e) {
return MalformedMessage(this.metadata).getWidget(context); return MalformedMessage(this.metadata).getWidget(context, key);
} }
} }
} }

View File

@ -1,5 +1,8 @@
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagebubble.dart'; import 'package:cwtch/widgets/messagebubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -28,12 +31,14 @@ class TextMessage extends Message {
} }
@override @override
Widget getWidget(BuildContext context) { Widget getWidget(BuildContext context, Key key) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString(); return MessageRow(
return MessageRow(MessageBubble(this.content), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); MessageBubble(this.content),
key: key,
);
}); });
} }
} }

View File

@ -1,3 +1,4 @@
import 'package:cwtch/model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ProfileServerListState extends ChangeNotifier { class ProfileServerListState extends ChangeNotifier {
@ -6,6 +7,7 @@ class ProfileServerListState extends ChangeNotifier {
void replace(Iterable<RemoteServerInfoState> newServers) { void replace(Iterable<RemoteServerInfoState> newServers) {
_servers.clear(); _servers.clear();
_servers.addAll(newServers); _servers.addAll(newServers);
resort();
notifyListeners(); notifyListeners();
} }
@ -14,23 +16,78 @@ class ProfileServerListState extends ChangeNotifier {
return idx >= 0 ? _servers[idx] : null; return idx >= 0 ? _servers[idx] : null;
} }
void updateServerCache(String onion, String status) { void updateServerState(String onion, String status) {
int idx = _servers.indexWhere((element) => element.onion == onion); int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) { if (idx >= 0) {
_servers[idx] = RemoteServerInfoState(onion: onion, status: status); _servers[idx].status = status;
} else { } else {
print("Tried to update server cache without a starting state...this is probably an error"); print("Tried to update server cache without a starting state...this is probably an error");
} }
resort();
notifyListeners(); notifyListeners();
} }
void resort() {
_servers.sort((RemoteServerInfoState a, RemoteServerInfoState b) {
// return -1 = a first in list
// return 1 = b first in list
// online v offline
if (a.status == "Synced" && b.status != "Synced") {
return -1;
} else if (a.status != "Synced" && b.status == "Synced") {
return 1;
}
// num of groups
if (a.groups.length > b.groups.length) {
return -1;
} else if (b.groups.length > a.groups.length) {
return 1;
}
return 0;
});
}
void clearGroups() {
_servers.map((server) => server.clearGroups());
}
void addGroup(ContactInfoState group) {
int idx = _servers.indexWhere((element) => element.onion == group.server);
if (idx >= 0) {
_servers[idx].addGroup(group);
}
}
List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
} }
class RemoteServerInfoState extends ChangeNotifier { class RemoteServerInfoState extends ChangeNotifier {
final String onion; final String onion;
final String status; final int identifier;
String status;
String description;
List<ContactInfoState> _groups = [];
RemoteServerInfoState({required this.onion, required this.identifier, required this.description, required this.status});
void updateDescription(String newDescription) {
this.description = newDescription;
notifyListeners();
}
void clearGroups() {
_groups = [];
}
void addGroup(ContactInfoState group) {
_groups.add(group);
notifyListeners();
}
List<ContactInfoState> get groups => _groups.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
RemoteServerInfoState({required this.onion, required this.status});
} }

View File

@ -24,12 +24,7 @@ class ServerListState extends ChangeNotifier {
if (idx >= 0) { if (idx >= 0) {
_servers[idx] = sis; _servers[idx] = sis;
} else { } else {
_servers.add(ServerInfoState(onion: onion, _servers.add(ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted));
serverBundle: serverBundle,
running: running,
description: description,
autoStart: autoStart,
isEncrypted: isEncrypted));
} }
notifyListeners(); notifyListeners();
} }
@ -37,7 +32,7 @@ class ServerListState extends ChangeNotifier {
void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) { void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
int idx = _servers.indexWhere((element) => element.onion == onion); int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) { if (idx >= 0) {
_servers[idx] = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted); _servers[idx] = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted);
} else { } else {
print("Tried to update server list without a starting state...this is probably an error"); print("Tried to update server list without a starting state...this is probably an error");
} }

View File

@ -12,6 +12,7 @@ const TapirGroupsExperiment = "tapir-groups-experiment";
const ServerManagementExperiment = "servers-experiment"; const ServerManagementExperiment = "servers-experiment";
const FileSharingExperiment = "filesharing"; const FileSharingExperiment = "filesharing";
const ImagePreviewsExperiment = "filesharing-images"; const ImagePreviewsExperiment = "filesharing-images";
const ClickableLinksExperiment = "clickable-links";
enum DualpaneMode { enum DualpaneMode {
Single, Single,

View File

@ -107,8 +107,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
labelText: AppLocalizations.of(context)!.yourDisplayName, labelText: AppLocalizations.of(context)!.yourDisplayName,
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value.isEmpty) {
// TODO l10n ize return AppLocalizations.of(context)!.displayNameTooltip;
return "Please enter a display name";
} }
return null; return null;
}, },
@ -296,11 +295,13 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
// Profile Editing // Profile Editing
if (ctrlrPass.value.text.isEmpty) { if (ctrlrPass.value.text.isEmpty) {
// Don't update password, only update name // Don't update password, only update name
Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text); Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
// At this points passwords have been validated to be the same and not empty // At this points passwords have been validated to be the same and not empty
// Update both password and name, even if name hasn't been changed... // Update both password and name, even if name hasn't been changed...
Provider.of<ProfileInfoState>(context, listen: false).nickname = ctrlrNick.value.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text); Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
final updatePasswordEvent = { final updatePasswordEvent = {
"EventType": "ChangePassword", "EventType": "ChangePassword",

View File

@ -53,7 +53,6 @@ class _AddEditServerViewState extends State<AddEditServerView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle), title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle),
@ -82,232 +81,222 @@ class _AddEditServerViewState extends State<AddEditServerView> {
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Container( child: Container(
margin: EdgeInsets.fromLTRB(30, 0, 30, 10), margin: EdgeInsets.fromLTRB(30, 0, 30, 10),
padding: EdgeInsets.fromLTRB(20, 0 , 20, 10), padding: EdgeInsets.fromLTRB(20, 0, 20, 10),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ // Onion
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
SizedBox(
height: 20,
),
SelectableText(serverInfoState.onion)
])),
// Onion // Description
Visibility( Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
visible: serverInfoState.onion.isNotEmpty, SizedBox(
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ height: 20,
SizedBox( ),
height: 20, CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
), Text(AppLocalizations.of(context)!.serverDescriptionDescription),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress), SizedBox(
SizedBox( height: 20,
height: 20, ),
), CwtchTextField(
SelectableText( controller: ctrlrDesc,
serverInfoState.onion labelText: AppLocalizations.of(context)!.fieldDescriptionLabel,
) autofocus: false,
])), )
]),
// Description SizedBox(
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ height: 20,
SizedBox( ),
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
Text(AppLocalizations.of(context)!.serverDescriptionDescription),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrDesc,
labelText: "Description",
autofocus: false,
)
]),
SizedBox( // Enabled
height: 20, Visibility(
), visible: serverInfoState.onion.isNotEmpty,
child: SwitchListTile(
title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription),
value: serverInfoState.running,
onChanged: (bool value) {
serverInfoState.setRunning(value);
if (value) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor()),
)),
// Enabled // Auto start
Visibility( SwitchListTile(
visible: serverInfoState.onion.isNotEmpty, title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())),
child: SwitchListTile( subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription),
title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor())), value: serverInfoState.autoStart,
subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription), onChanged: (bool value) {
value: serverInfoState.running, serverInfoState.setAutostart(value);
onChanged: (bool value) {
serverInfoState.setRunning(value);
if (value) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor()),
)),
// Auto start if (!serverInfoState.onion.isEmpty) {
SwitchListTile( Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())), }
subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription), },
value: serverInfoState.autoStart, activeTrackColor: settings.theme.defaultButtonActiveColor(),
onChanged: (bool value) { inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
serverInfoState.setAutostart(value); secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()),
),
if (! serverInfoState.onion.isEmpty) { // ***** Password *****
Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
}
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()),
),
// use password toggle
Visibility(
visible: serverInfoState.onion.isEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
SizedBox(
height: 20,
),
Checkbox(
value: usePassword,
fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor()),
activeColor: settings.current().defaultButtonActiveColor(),
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context)!.radioUsePassword,
style: TextStyle(color: settings.current().mainTextColor()),
),
SizedBox(
height: 20,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
usePassword ? AppLocalizations.of(context)!.encryptedServerDescription : AppLocalizations.of(context)!.plainServerDescription,
textAlign: TextAlign.center,
)),
SizedBox(
height: 20,
),
])),
// ***** Password ***** // current password
Visibility(
visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrOldPass,
autoFillHints: [AutofillHints.newPassword],
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.isEncrypted && serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (Provider.of<ErrorHandler>(context).deletedServerError == true) {
return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer;
}
return null;
},
),
SizedBox(
height: 20,
),
])),
// use password toggle // new passwords 1 & 2
Visibility( Visibility(
visible: serverInfoState.onion.isEmpty, // Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ visible: serverInfoState.onion.isEmpty && usePassword,
SizedBox( child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
height: 20, CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
), SizedBox(
Checkbox( height: 20,
value: usePassword, ),
fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor()), CwtchPasswordField(
activeColor: settings.current().defaultButtonActiveColor(), controller: ctrlrPass,
onChanged: _handleSwitchPassword, validator: (value) {
), // Password field can be empty when just updating the profile, not on creation
Text( if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
AppLocalizations.of(context)!.radioUsePassword, return AppLocalizations.of(context)!.passwordErrorEmpty;
style: TextStyle(color: settings.current().mainTextColor()), }
), if (value != ctrlrPass2.value.text) {
SizedBox( return AppLocalizations.of(context)!.passwordErrorMatch;
height: 20, }
), return null;
Padding( },
padding: EdgeInsets.symmetric(horizontal: 24), ),
child: Text( SizedBox(
usePassword ? AppLocalizations.of(context)!.encryptedServerDescription : AppLocalizations.of(context)!.plainServerDescription, height: 20,
textAlign: TextAlign.center, ),
)), CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
])), CwtchPasswordField(
controller: ctrlrPass2,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
}),
]),
),
SizedBox(
height: 20,
),
// current password Row(
Visibility( mainAxisAlignment: MainAxisAlignment.center,
visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted, children: [
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Expanded(
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel), child: ElevatedButton(
SizedBox( onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
height: 20, child: Text(
), serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton,
CwtchPasswordField( textAlign: TextAlign.center,
controller: ctrlrOldPass, ),
autoFillHints: [AutofillHints.newPassword], ),
validator: (value) { ),
// Password field can be empty when just updating the profile, not on creation ],
if (serverInfoState.isEncrypted && ),
serverInfoState.onion.isEmpty && Visibility(
value.isEmpty && visible: serverInfoState.onion.isNotEmpty,
usePassword) { child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
return AppLocalizations.of(context)!.passwordErrorEmpty; SizedBox(
} height: 20,
if (Provider.of<ErrorHandler>(context).deletedServerError == true) { ),
return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer; Tooltip(
} message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer,
return null; child: ElevatedButton.icon(
}, onPressed: () {
), showAlertDialog(context);
SizedBox( },
height: 20, icon: Icon(Icons.delete_forever),
), label: Text(AppLocalizations.of(context)!.deleteBtn),
])), ))
]))
// new passwords 1 & 2 // ***** END Password *****
Visibility( ]))))));
// Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
visible: serverInfoState.onion.isEmpty && usePassword,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass2.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
},
),
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
SizedBox(
height: 20,
),
CwtchPasswordField(
controller: ctrlrPass2,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch;
}
return null;
}),
]),
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
child: Text(
serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton,
textAlign: TextAlign.center,
),
),
),
],
),
Visibility(
visible: serverInfoState.onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
))
]))
// ***** END Password *****
]))))));
}); });
}); });
} }
@ -318,29 +307,20 @@ class _AddEditServerViewState extends State<AddEditServerView> {
// match (and are provided if the user has requested an encrypted profile). // match (and are provided if the user has requested an encrypted profile).
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
if (usePassword) { if (usePassword) {
Provider Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
.of<FlwtchState>(context, listen: false)
.cwtch
.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
} else { } else {
Provider Provider.of<FlwtchState>(context, listen: false).cwtch.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
.of<FlwtchState>(context, listen: false)
.cwtch
.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
void _savePressed() { void _savePressed() {
var server = Provider.of<ServerInfoState>(context, listen: false); var server = Provider.of<ServerInfoState>(context, listen: false);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
.cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
server.setDescription(ctrlrDesc.text); server.setDescription(ctrlrDesc.text);
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
// TODO support change password // TODO support change password
} }
@ -358,16 +338,11 @@ class _AddEditServerViewState extends State<AddEditServerView> {
Widget continueButton = ElevatedButton( Widget continueButton = ElevatedButton(
child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn), child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn),
onPressed: () { onPressed: () {
var onion = Provider var onion = Provider.of<ServerInfoState>(context, listen: false).onion;
.of<ServerInfoState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteServer(onion, Provider.of<ServerInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
.onion;
Provider
.of<FlwtchState>(context, listen: false)
.cwtch
.DeleteServer(onion, Provider.of<ServerInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
Future.delayed( Future.delayed(
const Duration(milliseconds: 500), const Duration(milliseconds: 500),
() { () {
if (globalErrorHandler.deletedServerSuccess) { if (globalErrorHandler.deletedServerSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
@ -395,4 +370,4 @@ class _AddEditServerViewState extends State<AddEditServerView> {
}, },
); );
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/views/profileserversview.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/views/torstatusview.dart'; import 'package:cwtch/views/torstatusview.dart';
import 'package:cwtch/widgets/contactrow.dart'; import 'package:cwtch/widgets/contactrow.dart';
@ -22,7 +23,7 @@ class ContactsView extends StatefulWidget {
} }
// selectConversation can be called from anywhere to set the active conversation // selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, String handle) { void selectConversation(BuildContext context, int handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts // 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; var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0; Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
@ -36,7 +37,7 @@ void selectConversation(BuildContext context, String handle) {
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle); if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
} }
void _pushMessageView(BuildContext context, String handle) { void _pushMessageView(BuildContext context, int handle) {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
@ -112,8 +113,15 @@ class _ContactsViewState extends State<ContactsView> {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion)); Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
})); }));
// Manage known Servers
// TODO servers if (Provider.of<Settings>(context, listen: false).isExperimentEnabled(ServerManagementExperiment)) {
actions.add(IconButton(
icon: Icon(CwtchIcons.dns_24px),
tooltip: AppLocalizations.of(context)!.manageKnownServersButton,
onPressed: () {
_pushServers();
}));
}
// Search contacts // Search contacts
actions.add(IconButton( actions.add(IconButton(
@ -163,12 +171,13 @@ class _ContactsViewState extends State<ContactsView> {
)); ));
} }
void _pushTorStatus() { void _pushServers() {
var profile = Provider.of<ProfileInfoState>(context);
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [Provider.value(value: Provider.of<FlwtchState>(context))], providers: [ChangeNotifierProvider(create: (context) => profile), Provider.value(value: Provider.of<FlwtchState>(context))],
child: TorStatusView(), child: ProfileServersView(),
); );
}, },
)); ));

View File

@ -34,8 +34,8 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
MultiProvider(providers: [ MultiProvider(providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)), ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")), value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", -1, "")),
], child: Container(key: Key(flwtch.selectedConversation??"never_this"), child: MessageView())), ], child: Container(key: Key(flwtch.selectedConversation!.toString()), child: MessageView())),
), ),
], ],
); );

View File

@ -192,9 +192,8 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()), secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
), ),
Visibility( Visibility(
visible: !Platform.isAndroid && !Platform.isIOS, visible: !Platform.isAndroid && !Platform.isIOS,
child: child: SwitchListTile(
SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.settingServersDescription), subtitle: Text(AppLocalizations.of(context)!.settingServersDescription),
value: settings.isExperimentEnabled(ServerManagementExperiment), value: settings.isExperimentEnabled(ServerManagementExperiment),
@ -228,6 +227,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()), secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()),
), ),
<<<<<<< HEAD
Visibility( Visibility(
visible: settings.isExperimentEnabled(FileSharingExperiment), visible: settings.isExperimentEnabled(FileSharingExperiment),
child: Column(children:[ child: Column(children:[
@ -259,6 +259,22 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
}, },
), ),
), ),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.enableExperimentClickableLinks, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.experimentClickableLinksDescription),
value: settings.isExperimentEnabled(ClickableLinksExperiment),
onChanged: (bool value) {
if (value) {
settings.enableExperiment(ClickableLinksExperiment);
} else {
settings.disableExperiment(ClickableLinksExperiment);
}
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonActiveColor(),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(Icons.link, color: settings.current().mainTextColor()),
),
]), ]),
), ),
], ],

View File

@ -78,9 +78,9 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
readonly: false, readonly: false,
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text; Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text); Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, handle, "profile.name", ctrlrNick.text);
// todo translations // todo translations
final snackBar = SnackBar(content: Text("Group Nickname changed successfully")); final snackBar = SnackBar(content: Text("Group Nickname changed successfully"));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
@ -140,7 +140,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
@ -195,10 +195,11 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
child: Text(AppLocalizations.of(context)!.yesLeave), child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle); Provider.of<ProfileInfoState>(context, listen: false).contactList.removeContact(identifier);
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, identifier);
Future.delayed(Duration(milliseconds: 500), () { Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null; Provider.of<AppState>(context, listen: false).selectedConversation = null;
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/quotedmessage.dart'; import 'package:cwtch/models/messages/quotedmessage.dart';
@ -33,7 +34,7 @@ class MessageView extends StatefulWidget {
class _MessageViewState extends State<MessageView> { class _MessageViewState extends State<MessageView> {
final ctrlrCompose = TextEditingController(); final ctrlrCompose = TextEditingController();
final focusNode = FocusNode(); final focusNode = FocusNode();
String selectedContact = ""; int selectedContact = -1;
ItemPositionsListener scrollListener = ItemPositionsListener.create(); ItemPositionsListener scrollListener = ItemPositionsListener.create();
ItemScrollController scrollController = ItemScrollController(); ItemScrollController scrollController = ItemScrollController();
@ -41,10 +42,10 @@ class _MessageViewState extends State<MessageView> {
void initState() { void initState() {
scrollListener.itemPositions.addListener(() { scrollListener.itemPositions.addListener(() {
if (scrollListener.itemPositions.value.length != 0 && if (scrollListener.itemPositions.value.length != 0 &&
Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true && Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true &&
scrollListener.itemPositions.value.any((element) => element.index == 0)) { scrollListener.itemPositions.value.any((element) => element.index == 0)) {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0; Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false; Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
} }
}); });
super.initState(); super.initState();
@ -168,7 +169,7 @@ class _MessageViewState extends State<MessageView> {
if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<AppState>(context, listen: false).selectedIndex != null) { if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<AppState>(context, listen: false).selectedIndex != null) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.GetMessage(Provider.of<AppState>(context, listen: false).selectedProfile!, Provider.of<AppState>(context, listen: false).selectedConversation!, .GetMessageByID(Provider.of<AppState>(context, listen: false).selectedProfile!, Provider.of<AppState>(context, listen: false).selectedConversation!,
Provider.of<AppState>(context, listen: false).selectedIndex!) Provider.of<AppState>(context, listen: false).selectedIndex!)
.then((data) { .then((data) {
try { try {
@ -180,7 +181,7 @@ class _MessageViewState extends State<MessageView> {
ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage); ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm)); .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
} catch (e) {} } catch (e) {}
Provider.of<AppState>(context, listen: false).selectedIndex = null; Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper(); _sendMessageHelper();
@ -189,7 +190,7 @@ class _MessageViewState extends State<MessageView> {
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text); ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm)); .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
_sendMessageHelper(); _sendMessageHelper();
} }
} }
@ -198,14 +199,14 @@ class _MessageViewState extends State<MessageView> {
void _sendInvitation([String? ignoredParam]) { void _sendInvitation([String? ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact); .SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact);
_sendMessageHelper(); _sendMessageHelper();
} }
void _sendFile(String filePath) { void _sendFile(String filePath) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, filePath); .ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath);
_sendMessageHelper(); _sendMessageHelper();
} }
@ -213,10 +214,10 @@ class _MessageViewState extends State<MessageView> {
ctrlrCompose.clear(); ctrlrCompose.clear();
focusNode.requestFocus(); focusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 80), () { Future.delayed(const Duration(milliseconds: 80), () {
Provider.of<ContactInfoState>(context, listen: false).totalMessages++; Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(Provider.of<ContactInfoState>(context, listen: false).identifier)?.bumpMessageCache();
Provider.of<ContactInfoState>(context, listen: false).newMarker++; Provider.of<ContactInfoState>(context, listen: false).newMarker++;
// Resort the contact list... // Resort the contact list...
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).onion, DateTime.now()); Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).identifier, DateTime.now());
}); });
} }
@ -268,7 +269,8 @@ class _MessageViewState extends State<MessageView> {
var children; var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) { if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
var quoted = FutureBuilder( var quoted = FutureBuilder(
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!), future:
messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, Provider.of<AppState>(context).selectedIndex!, byID: true),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
var message = snapshot.data! as Message; var message = snapshot.data! as Message;
@ -352,7 +354,7 @@ class _MessageViewState extends State<MessageView> {
return contact.onion != Provider.of<ContactInfoState>(context).onion; return contact.onion != Provider.of<ContactInfoState>(context).onion;
}, onChanged: (newVal) { }, onChanged: (newVal) {
setState(() { setState(() {
this.selectedContact = newVal; this.selectedContact = Provider.of<ProfileInfoState>(context).contactList.findContact(newVal)!.identifier;
}); });
})), })),
SizedBox( SizedBox(
@ -361,7 +363,7 @@ class _MessageViewState extends State<MessageView> {
ElevatedButton( ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn), child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
onPressed: () { onPressed: () {
if (this.selectedContact != "") { if (this.selectedContact != -1) {
this._sendInvitation(); this._sendInvitation();
} }
Navigator.pop(bcontext); Navigator.pop(bcontext);

View File

@ -69,14 +69,9 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
readonly: false, readonly: false,
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var onion = Provider.of<ContactInfoState>(context, listen: false).onion; var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text; Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
final setPeerAttribute = { Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, conversation, "profile.name", ctrlrNick.text);
"EventType": "SetPeerAttribute",
"Data": {"RemotePeer": onion, "Key": "local.name", "Data": ctrlrNick.text},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, },
@ -200,7 +195,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
@ -239,7 +234,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
child: Text(AppLocalizations.of(context)!.yesLeave), child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle); Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle);

View File

@ -57,9 +57,9 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
SizedBox( SizedBox(
width: 10, width: 10,
), ),
Expanded(child: Text(MediaQuery.of(context).size.width > 600 ? Expanded(
AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort, child: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort,
style: TextStyle(color: settings.current().mainTextColor()))) style: TextStyle(color: settings.current().mainTextColor())))
]), ]),
actions: getActions(), actions: getActions(),
), ),
@ -238,7 +238,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
if (tiles.isEmpty) { if (tiles.isEmpty) {
return Center( return Center(
child: Text( child: Text(
AppLocalizations.of(context)!.unlockProfileTip, AppLocalizations.of(context)!.unlockProfileTip,
textAlign: TextAlign.center, textAlign: TextAlign.center,
)); ));
} }

View File

@ -0,0 +1,155 @@
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/remoteserverrow.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../cwtch_icons_icons.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
class ProfileServersView extends StatefulWidget {
@override
_ProfileServersView createState() => _ProfileServersView();
}
class _ProfileServersView extends State<ProfileServersView> {
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
var knownServers = Provider.of<ProfileInfoState>(context).serverList.servers.map<String>((RemoteServerInfoState remoteServer) { return remoteServer.onion + ".onion"; }).toSet();
var importServerList = Provider.of<ServerListState>(context).servers.where((server) => !knownServers.contains(server.onion) ).map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
return DropdownMenuItem<String>(
value: serverInfo.onion,
child: Text(
serverInfo.description.isNotEmpty ? serverInfo.description : serverInfo.onion,
overflow: TextOverflow.ellipsis,
),
);
}).toList();
importServerList.insert(0, DropdownMenuItem<String>(
value: "",
child: Text(AppLocalizations.of(context)!.importLocalServerSelectText)));
return Scaffold(
appBar: AppBar(
title: Text(MediaQuery
.of(context)
.size
.width > 600 ? AppLocalizations.of(context)!.manageKnownServersLong : AppLocalizations.of(context)!.manageKnownServersShort),
),
body: Consumer<ProfileInfoState>(builder: (context, profile, child) {
ProfileServerListState servers = profile.serverList;
final tiles = servers.servers.map((RemoteServerInfoState server) {
return ChangeNotifierProvider<RemoteServerInfoState>.value(
value: server,
builder: (context, child) => RepaintBoundary(child: RemoteServerRow()),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
final importCard = Card( child: ListTile(
title: Text(AppLocalizations.of(context)!.importLocalServerLabel),
leading: Icon(CwtchIcons.add_circle_24px , color: Provider.of<Settings>(context).current().mainTextColor()),
trailing: DropdownButton(
onChanged: (String? importServer) {
if (importServer!.isNotEmpty) {
var server = Provider.of<ServerListState>(context).getServer(importServer)!;
showImportConfirm(context, profile.onion, server.onion, server.description, server.serverBundle);
}
},
value: "",
items: importServerList,
)));
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
return Scrollbar(
isAlwaysShown: true,
child: SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child:
Container(
margin: EdgeInsets.fromLTRB(5, 0, 5, 10),
padding: EdgeInsets.fromLTRB(5, 0, 5, 10),
child: Column(children: [
if (importServerList.length > 1) importCard,
Column( children: divided )
]))));});
return ListView(children: divided);
},
));
}
showImportConfirm(BuildContext context, String profileHandle, String serverHandle, String serverDesc, String bundle) {
var serverLabel = serverDesc.isNotEmpty ? serverDesc : serverHandle;
serverHandle = serverHandle.substring(0, serverHandle.length-6 ); // remove '.onion'
// 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(AppLocalizations.of(context)!.importLocalServerButton.replaceAll("%1", serverLabel)),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileHandle, bundle);
// Wait 500ms and hope the server is imported and add it's description in the UI and as an attribute
Future.delayed(const Duration(milliseconds: 500), () {
var profile = Provider.of<ProfileInfoState>(context);
if (profile.serverList.getServer(serverHandle) != null) {
profile.serverList.getServer(serverHandle)?.updateDescription(
serverDesc);
Provider
.of<FlwtchState>(context, listen: false)
.cwtch
.SetConversationAttribute(profile.onion, profile.serverList
.getServer(serverHandle)
!.identifier, "server.description", serverDesc);
}
});
Navigator.of(context).pop();
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.importLocalServerButton.replaceAll("%1", serverLabel)),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}

View File

@ -0,0 +1,151 @@
import 'dart:convert';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/widgets/buttontextfield.dart';
import 'package:cwtch/widgets/contactrow.dart';
import 'package:cwtch/widgets/cwtchlabel.dart';
import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/textfield.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../errorHandler.dart';
import '../main.dart';
import '../config.dart';
import '../model.dart';
/// Pane to add or edit a server
class RemoteServerView extends StatefulWidget {
const RemoteServerView();
@override
_RemoteServerViewState createState() => _RemoteServerViewState();
}
class _RemoteServerViewState extends State<RemoteServerView> {
final _formKey = GlobalKey<FormState>();
final ctrlrDesc = TextEditingController(text: "");
@override
void initState() {
super.initState();
var serverInfoState = Provider.of<RemoteServerInfoState>(context, listen: false);
if (serverInfoState.description.isNotEmpty) {
ctrlrDesc.text = serverInfoState.description;
}
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer3<ProfileInfoState, RemoteServerInfoState, Settings>(builder: (context, profile, serverInfoState, settings, child) {
return Scaffold(
appBar: AppBar(
title: Text(ctrlrDesc.text.isNotEmpty ? ctrlrDesc.text : serverInfoState.onion)
),
body: Container(
margin: EdgeInsets.fromLTRB(30, 0, 30, 10),
padding: EdgeInsets.fromLTRB(20, 0, 20, 10),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
SizedBox(
height: 20,
),
SelectableText(
serverInfoState.onion
),
// Description
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
Text(AppLocalizations.of(context)!.serverDescriptionDescription),
SizedBox(
height: 20,
),
CwtchButtonTextField(
controller: ctrlrDesc,
readonly: false,
tooltip: AppLocalizations.of(context)!.saveBtn,
labelText: AppLocalizations.of(context)!.fieldDescriptionLabel,
icon: Icon(Icons.save),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profile.onion, serverInfoState.identifier, "server.description", ctrlrDesc.text);
serverInfoState.updateDescription(ctrlrDesc.text);
},
),
SizedBox(
height: 20,
),
Padding(padding: EdgeInsets.all(8), child: Text( AppLocalizations.of(context)!.groupsOnThisServerLabel),),
Expanded(child: _buildGroupsList(serverInfoState))
])));
});
}
Widget _buildGroupsList(RemoteServerInfoState serverInfoState) {
final tiles = serverInfoState.groups.map((ContactInfoState group) {
return ChangeNotifierProvider<ContactInfoState>.value(
value: group,
builder: (context, child) => RepaintBoundary(child: _buildGroupRow(group)), // ServerRow()),
);
},
);
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
var size = MediaQuery.of(context).size;
int cols = ((size.width - 50) / 500).ceil();
final double itemHeight = 60; // magic arbitary
final double itemWidth = (size.width - 50 /* magic padding guess */) / cols;
return GridView.count(crossAxisCount: cols, childAspectRatio: (itemWidth / itemHeight), children: divided);
}
Widget _buildGroupRow(ContactInfoState group) {
return Padding(
padding: const EdgeInsets.all(6.0), //border size
child: Column(
children: [
Text(
group.nickname,
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: Provider.of<Settings>(context).theme.portraitOnlineBorderColor()),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
group.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Provider.of<Settings>(context).theme.portraitOnlineBorderColor()),
)))
])
);
}
}

View File

@ -30,44 +30,45 @@ class _ServersView extends State<ServersView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), title: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort),
actions: getActions(), actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddServer,
tooltip: AppLocalizations.of(context)!.addServerTooltip,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
), ),
), floatingActionButton: FloatingActionButton(
body: Consumer<ServerListState>( onPressed: _pushAddServer,
builder: (context, svrs, child) { tooltip: AppLocalizations.of(context)!.addServerTooltip,
final tiles = svrs.servers.map((ServerInfoState server) { child: Icon(
return ChangeNotifierProvider<ServerInfoState>.value( Icons.add,
value: server, semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
builder: (context, child) => RepaintBoundary(child: ServerRow()), ),
),
body: Consumer<ServerListState>(
builder: (context, svrs, child) {
final tiles = svrs.servers.map(
(ServerInfoState server) {
return ChangeNotifierProvider<ServerInfoState>.value(
value: server,
builder: (context, child) => RepaintBoundary(child: ServerRow()),
);
},
); );
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return Center(
child: Text(
AppLocalizations.of(context)!.unlockServerTip,
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
}, },
); ));
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return Center(
child: Text(
AppLocalizations.of(context)!.unlockServerTip,
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
},
));
} }
List<Widget> getActions() { List<Widget> getActions() {
@ -93,41 +94,41 @@ class _ServersView extends State<ServersView> {
padding: MediaQuery.of(context).viewInsets, padding: MediaQuery.of(context).viewInsets,
child: RepaintBoundary( child: RepaintBoundary(
child: Container( child: Container(
height: 200, // bespoke value courtesy of the [TextField] docs height: 200, // bespoke value courtesy of the [TextField] docs
child: Center( child: Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(10.0), padding: EdgeInsets.all(10.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text(AppLocalizations.of(context)!.enterServerPassword), Text(AppLocalizations.of(context)!.enterServerPassword),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchPasswordField( CwtchPasswordField(
autofocus: true, autofocus: true,
controller: ctrlrPassword, controller: ctrlrPassword,
action: unlock, action: unlock,
validator: (value) {}, validator: (value) {},
), ),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(), Spacer(),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock), child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock),
onPressed: () { onPressed: () {
unlock(ctrlrPassword.value.text); unlock(ctrlrPassword.value.text);
}, },
)), )),
Spacer() Spacer()
]), ]),
], ],
))), ))),
))); )));
}); });
} }
@ -141,9 +142,11 @@ class _ServersView extends State<ServersView> {
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ChangeNotifierProvider<ServerInfoState>( providers: [
create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true), ChangeNotifierProvider<ServerInfoState>(
)], create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true),
)
],
child: AddEditServerView(), child: AddEditServerView(),
); );
}, },

View File

@ -5,12 +5,13 @@ import 'package:provider/provider.dart';
// Provides a styled Text Field for use in Form Widgets. // Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchButtonTextField extends StatefulWidget { class CwtchButtonTextField extends StatefulWidget {
CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true}); CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true, this.labelText});
final TextEditingController controller; final TextEditingController controller;
final Function()? onPressed; final Function()? onPressed;
final Icon icon; final Icon icon;
final String tooltip; final String tooltip;
final bool readonly; final bool readonly;
String? labelText;
@override @override
_CwtchButtonTextFieldState createState() => _CwtchButtonTextFieldState(); _CwtchButtonTextFieldState createState() => _CwtchButtonTextFieldState();
@ -39,6 +40,8 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
focusNode: _focusNode, focusNode: _focusNode,
enableIMEPersonalizedLearning: false, enableIMEPersonalizedLearning: false,
decoration: InputDecoration( decoration: InputDecoration(
labelText: widget.labelText,
labelStyle: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: widget.onPressed, onPressed: widget.onPressed,
icon: widget.icon, icon: widget.icon,

View File

@ -105,24 +105,26 @@ class _ContactRowState extends State<ContactRow> {
), ),
]), ]),
onTap: () { onTap: () {
selectConversation(context, contact.onion); selectConversation(context, contact.identifier);
}, },
)); ));
} }
void _btnApprove() { void _btnApprove() {
// Update the UI
Provider.of<ContactInfoState>(context, listen: false).authorization = ContactAuthorization.approved;
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.AcceptContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion); .AcceptContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier);
} }
void _btnReject() { void _btnReject() {
ContactInfoState contact = Provider.of<ContactInfoState>(context, listen: false); ContactInfoState contact = Provider.of<ContactInfoState>(context, listen: false);
if (contact.isGroup == true) { if (contact.isGroup == true) {
Provider.of<FlwtchState>(context, listen: false).cwtch.RejectInvite(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.onion); // FIXME This flow is incrorect. Groups never just show up on the contact list anymore
Provider.of<ProfileInfoState>(context, listen: false).removeContact(contact.onion); Provider.of<ProfileInfoState>(context, listen: false).removeContact(contact.onion);
} else { } else {
Provider.of<FlwtchState>(context, listen: false).cwtch.BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.onion); Provider.of<FlwtchState>(context, listen: false).cwtch.BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, contact.identifier);
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:cwtch/config.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:file_picker_desktop/file_picker_desktop.dart'; import 'package:file_picker_desktop/file_picker_desktop.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -43,7 +44,7 @@ class FileBubbleState extends State<FileBubble> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var flagStarted = Provider.of<MessageMetadata>(context).flags & 0x02 > 0; var flagStarted = Provider.of<MessageMetadata>(context).attributes["file-downloaded"] == "true";
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment); var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
@ -51,7 +52,7 @@ class FileBubbleState extends State<FileBubble> {
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -102,15 +103,15 @@ class FileBubbleState extends State<FileBubble> {
} else if (flagStarted) { } else if (flagStarted) {
// in this case, the download was done in a previous application launch, // in this case, the download was done in a previous application launch,
// so we probably have to request an info lookup // so we probably have to request an info lookup
if (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey()) ) { if (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey())) {
wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F'); wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F');
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey()); Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey());
} else { } else {
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? ""; var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? "";
wdgDecorations = Column( wdgDecorations = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),
children:[Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))] ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))
); ]);
} }
} else { } else {
wdgDecorations = Center( wdgDecorations = Center(
@ -159,15 +160,17 @@ class FileBubbleState extends State<FileBubble> {
String? selectedFileName; String? selectedFileName;
File? file; File? file;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle; var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion; var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
if (Platform.isAndroid) { if (Platform.isAndroid) {
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02); Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02; //Provider.of<MessageMetadata>(context, listen: false).attributes |= 0x02;
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey()); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey());
}
} else { } else {
try { try {
selectedFileName = await saveFile( selectedFileName = await saveFile(
@ -175,12 +178,15 @@ class FileBubbleState extends State<FileBubble> {
); );
if (selectedFileName != null) { if (selectedFileName != null) {
file = File(selectedFileName); file = File(selectedFileName);
print("saving to " + file.path); EnvironmentConfig.debugLog("saving to " + file.path);
var manifestPath = file.path + ".manifest"; var manifestPath = file.path + ".manifest";
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02); Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02; //Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
}
} }
} catch (e) { } catch (e) {
print(e); print(e);
@ -190,7 +196,7 @@ class FileBubbleState extends State<FileBubble> {
void _btnResume() async { void _btnResume() async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle; var handle = Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier;
Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey()); Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey());
Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey()); Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey());
} }

View File

@ -36,16 +36,16 @@ class InvitationBubbleState extends State<InvitationBubble> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isGroup = widget.overlay == InviteGroupOverlay; var isGroup = widget.overlay == InviteGroupOverlay;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget) != null; isAccepted = Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget) != null;
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment); var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageMetadata>(context).flags & 0x01 == 0x01; rejected = Provider.of<MessageMetadata>(context).attributes["rejected-invite"] == "true";
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -69,7 +69,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) ? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning)
: fromMe : fromMe
? senderInviteChrome( ? senderInviteChrome(
AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(widget.inviteTarget)!.nickname : widget.inviteTarget) AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget)!.nickname : widget.inviteTarget)
: (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget)); : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget));
Widget wdgDecorations; Widget wdgDecorations;
@ -128,10 +128,10 @@ class InvitationBubbleState extends State<InvitationBubble> {
void _btnReject() { void _btnReject() {
setState(() { setState(() {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var contact = Provider.of<ContactInfoState>(context, listen: false).onion; var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex; var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01); Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "rejected-invite", "true");
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01; //Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01;
}); });
} }

View File

@ -3,9 +3,13 @@ import 'dart:io';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.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:provider/provider.dart';
import '../model.dart'; import '../model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.dart'; import 'messagebubbledecorations.dart';
@ -28,6 +32,7 @@ class MessageBubbleState extends State<MessageBubble> {
var prettyDate = ""; var prettyDate = "";
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
// var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString(); // var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp; DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal()); prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal());
@ -35,7 +40,7 @@ class MessageBubbleState extends State<MessageBubble> {
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -45,16 +50,40 @@ class MessageBubbleState extends State<MessageBubble> {
var wdgSender = SelectableText(senderDisplayStr, var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())); style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText( var wdgMessage;
widget.content + '\u202F',
//key: Key(myKey), if (!showClickableLinks) {
focusNode: _focus, wdgMessage = SelectableText(
style: TextStyle( widget.content + '\u202F',
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(), //key: Key(myKey),
), focusNode: _focus,
textAlign: TextAlign.left, style: TextStyle(
textWidthBasis: TextWidthBasis.longestLine, 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(humanize: false, removeWww: false, 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 wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate); var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
@ -90,4 +119,57 @@ class MessageBubbleState extends State<MessageBubble> {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); 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)!.copiedClipboardNotification),
);
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';
}
},
),
),
]),
],
)),
));
});
}
} }

View File

@ -76,18 +76,18 @@ class _MessageListState extends State<MessageList> {
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... 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) { itemBuilder: (itemBuilderContext, index) {
var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion; var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).identifier;
var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1; var messageIndex = index;
return FutureBuilder( return FutureBuilder(
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
var message = snapshot.data as Message; var message = snapshot.data as Message;
// Already includes MessageRow,, var key = Provider.of<ContactInfoState>(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID);
return message.getWidget(context); return message.getWidget(context, key);
} else { } else {
return Text(''); //MessageLoadingBubble(); return MessageLoadingBubble();
} }
}, },
); );

View File

@ -1,9 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import 'package:intl/intl.dart';
import '../settings.dart';
class MessageLoadingBubble extends StatefulWidget { class MessageLoadingBubble extends StatefulWidget {
@override @override

View File

@ -15,6 +15,7 @@ import '../settings.dart';
class MessageRow extends StatefulWidget { class MessageRow extends StatefulWidget {
final Widget child; final Widget child;
MessageRow(this.child, {Key? key}) : super(key: key); MessageRow(this.child, {Key? key}) : super(key: key);
@override @override
@ -28,9 +29,12 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
late Alignment _dragAlignment = Alignment.center; late Alignment _dragAlignment = Alignment.center;
Alignment _dragAffinity = Alignment.center; Alignment _dragAffinity = Alignment.center;
late int index;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
index = Provider.of<MessageMetadata>(context, listen: false).messageID;
_controller = AnimationController(vsync: this); _controller = AnimationController(vsync: this);
_controller.addListener(() { _controller.addListener(() {
setState(() { setState(() {
@ -41,15 +45,17 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
@override @override
void dispose() { void dispose() {
_controller.dispose(); if (_controller != null) {
_controller.dispose();
}
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isContact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle) != null; var isContact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle) != null;
var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false; var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false;
var actualMessage = Flexible(flex: 3, fit: FlexFit.loose, child: widget.child); var actualMessage = Flexible(flex: 3, fit: FlexFit.loose, child: widget.child);
_dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft; _dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft;
@ -60,7 +66,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {
@ -69,7 +75,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
} }
Widget wdgIcons = Visibility( Widget wdgIcons = Visibility(
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageIndex, visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
maintainSize: true, maintainSize: true,
maintainAnimation: true, maintainAnimation: true,
maintainState: true, maintainState: true,
@ -77,7 +83,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
child: IconButton( child: IconButton(
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage, tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
onPressed: () { onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex; Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
}, },
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor()))); icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
Widget wdgSpacer = Flexible(child: SizedBox(width: 60, height: 10)); Widget wdgSpacer = Flexible(child: SizedBox(width: 60, height: 10));
@ -163,7 +169,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
// For desktop... // For desktop...
onHover: (event) { onHover: (event) {
setState(() { setState(() {
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex; Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
}); });
}, },
onExit: (event) { onExit: (event) {
@ -185,7 +191,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
}, },
onPanEnd: (details) { onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size); _runAnimation(details.velocity.pixelsPerSecond, size);
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex; Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
}, },
child: Padding( child: Padding(
padding: EdgeInsets.all(2), padding: EdgeInsets.all(2),
@ -198,8 +204,10 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
children: widgetRow, children: widgetRow,
))))); )))));
var mark = Provider.of<ContactInfoState>(context).newMarker; var mark = Provider.of<ContactInfoState>(context).newMarker;
if (mark > 0 && mark == Provider.of<ContactInfoState>(context).totalMessages - Provider.of<MessageMetadata>(context).messageIndex) { if (mark > 0 &&
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Align(alignment:Alignment.center ,child:_bubbleNew()), mr]); Provider.of<ContactInfoState>(context).messageCache.length > mark &&
Provider.of<ContactInfoState>(context).messageCache[mark - 1]?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
} else { } else {
return mr; return mr;
} }
@ -209,9 +217,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(), color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
border: Border.all( border: Border.all(color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(), width: 1),
color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
width: 1),
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(8), topLeft: Radius.circular(8),
topRight: Radius.circular(8), topRight: Radius.circular(8),
@ -219,9 +225,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
bottomRight: Radius.circular(8), bottomRight: Radius.circular(8),
), ),
), ),
child: Padding( child: Padding(padding: EdgeInsets.all(9.0), child: Text(AppLocalizations.of(context)!.newMessagesLabel)));
padding: EdgeInsets.all(9.0),
child: Text(AppLocalizations.of(context)!.newMessagesLabel)));
} }
void _runAnimation(Offset pixelsPerSecond, Size size) { void _runAnimation(Offset pixelsPerSecond, Size size) {
@ -249,12 +253,17 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
} }
void _btnGoto() { void _btnGoto() {
selectConversation(context, Provider.of<MessageMetadata>(context, listen: false).senderHandle); var id = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle)?.identifier;
if (id == null) {
// Can't happen
} else {
selectConversation(context, id);
}
} }
void _btnAdd() { void _btnAdd() {
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle; var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") { if (sender == "") {
print("sender not yet loaded"); print("sender not yet loaded");
return; return;
} }

View File

@ -34,7 +34,7 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
// If the sender is not us, then we want to give them a nickname... // If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe) { if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle); ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) { if (contact != null) {
senderDisplayStr = contact.nickname; senderDisplayStr = contact.nickname;
} else { } else {

View File

@ -0,0 +1,78 @@
import 'package:cwtch/main.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/views/addeditservers.dart';
import 'package:cwtch/views/remoteserverview.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../cwtch_icons_icons.dart';
import '../errorHandler.dart';
import '../model.dart';
import '../settings.dart';
class RemoteServerRow extends StatefulWidget {
@override
_RemoteServerRowState createState() => _RemoteServerRowState();
}
class _RemoteServerRowState extends State<RemoteServerRow> {
@override
Widget build(BuildContext context) {
var server = Provider.of<RemoteServerInfoState>(context);
var description = server.description.isNotEmpty ? server.description : server.onion;
var running = server.status == "Synced";
return Consumer<ProfileInfoState>(
builder: (context, profile, child) {
return Card(clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(0.0),
child: InkWell(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(6.0), //border size
child: Icon(CwtchIcons.dns_24px,
color: running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
size: 64)
),
Expanded(
child: Column(
children: [
Text(
description,
semanticsLabel: description,
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
server.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
)))
],
)),
]),
onTap: () {
Navigator.of(context).push(MaterialPageRoute<void>(
settings: RouteSettings(name: "remoteserverview"),
builder: (BuildContext context) {
return MultiProvider(
providers: [Provider.value(value: profile), ChangeNotifierProvider(create: (context) => server), Provider.value(value: Provider.of<FlwtchState>(context))],
child: RemoteServerView(),
);
}));
}
));});
}
}

View File

@ -21,40 +21,38 @@ class _ServerRowState extends State<ServerRow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var server = Provider.of<ServerInfoState>(context); var server = Provider.of<ServerInfoState>(context);
return Card(clipBehavior: Clip.antiAlias, return Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(0.0), margin: EdgeInsets.all(0.0),
child: InkWell( child: InkWell(
child: Row( child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding( Padding(
padding: const EdgeInsets.all(6.0), //border size padding: const EdgeInsets.all(6.0), //border size
child: Icon(CwtchIcons.dns_24px, child: Icon(CwtchIcons.dns_24px,
color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(), color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(), size: 64)),
size: 64)
),
Expanded( Expanded(
child: Column( child: Column(
children: [ children: [
Text( Text(
server.description, server.description,
semanticsLabel: server.description, semanticsLabel: server.description,
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()), style: Provider.of<FlwtchState>(context)
.biggerFont
.apply(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
softWrap: true,
overflow: TextOverflow.ellipsis,
),
Visibility(
visible: !Provider.of<Settings>(context).streamerMode,
child: ExcludeSemantics(
child: Text(
server.onion,
softWrap: true, softWrap: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), style: TextStyle(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
Visibility( )))
visible: !Provider.of<Settings>(context).streamerMode, ],
child: ExcludeSemantics( )),
child: Text(
server.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
)))
],
)),
// Copy server button // Copy server button
IconButton( IconButton(
@ -75,23 +73,27 @@ class _ServerRowState extends State<ServerRow> {
_pushEditServer(server); _pushEditServer(server);
}, },
) )
]),
onTap: () {
]))); _pushEditServer(server);
}
));
} }
void _pushEditServer(ServerInfoState server ) { void _pushEditServer(ServerInfoState server) {
Provider.of<ErrorHandler>(context).reset(); Provider.of<ErrorHandler>(context).reset();
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
settings: RouteSettings(name: "serveraddedit"), settings: RouteSettings(name: "serveraddedit"),
builder: (BuildContext context) { builder: (BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ChangeNotifierProvider<ServerInfoState>( providers: [
create: (_) => server, ChangeNotifierProvider<ServerInfoState>(
)], create: (_) => server,
)
],
child: AddEditServerView(), child: AddEditServerView(),
); );
}, },
)); ));
} }
} }

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -7,8 +7,10 @@ import Foundation
import package_info_plus_macos import package_info_plus_macos
import path_provider_macos import path_provider_macos
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -4,11 +4,14 @@ PODS:
- FlutterMacOS - FlutterMacOS
- path_provider_macos (0.0.1): - path_provider_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
FlutterMacOS: FlutterMacOS:
@ -17,12 +20,15 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
path_provider_macos: path_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
COCOAPODS: 1.9.3 COCOAPODS: 1.11.2

View File

@ -35,7 +35,7 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -125,6 +125,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_linkify:
dependency: "direct main"
description:
name: flutter_linkify
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -189,6 +196,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.3"
linkify:
dependency: transitive
description:
name: linkify
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -403,7 +417,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.4.7"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -411,13 +425,55 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.12"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:
@ -447,5 +503,5 @@ packages:
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.13.0 <3.0.0" dart: ">=2.14.0 <3.0.0"
flutter: ">=2.0.0" flutter: ">=2.5.0"

View File

@ -43,6 +43,8 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0 scrollable_positioned_list: ^0.2.0-nullsafety.0
file_picker: ^4.0.1 file_picker: ^4.0.1
file_picker_desktop: ^1.1.0 file_picker_desktop: ^1.1.0
flutter_linkify: ^5.0.2
url_launcher: ^6.0.12
dev_dependencies: dev_dependencies:
msix: ^2.1.3 msix: ^2.1.3

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)