From ec1dd05ba1ed81ad9837c87f2a4316fb8c478c40 Mon Sep 17 00:00:00 2001 From: Nima Boscarino Date: Fri, 5 Nov 2021 22:38:45 -0700 Subject: [PATCH 01/28] WIP: add experimental clickable links with dialog (copy / open). Bug remaining for selectable text --- android/app/src/main/AndroidManifest.xml | 5 + lib/settings.dart | 1 + lib/views/globalsettingsview.dart | 16 +++ lib/widgets/messagebubble.dart | 100 ++++++++++++++++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 8 +- pubspec.lock | 62 ++++++++++- pubspec.yaml | 2 + 8 files changed, 182 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ec51df3e..d562b149 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,11 @@ + + + + + diff --git a/lib/settings.dart b/lib/settings.dart index c0955489..8573aa97 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; const TapirGroupsExperiment = "tapir-groups-experiment"; const FileSharingExperiment = "filesharing"; +const ClickableLinksExperiment = "clickable-links"; enum DualpaneMode { Single, diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 20695220..e20f2970 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -204,6 +204,22 @@ class _GlobalSettingsViewState extends State { inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()), ), + SwitchListTile( + title: Text("Enable Clickable Links", style: TextStyle(color: settings.current().mainTextColor())), + subtitle: Text("The clickable links experiment allows you to click on URLs shared in messages."), + 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()), + ), ], )), AboutListTile( diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 2ba8fe0c..39a8f9a3 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -3,9 +3,13 @@ import 'dart:io'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import '../model.dart'; import 'package:intl/intl.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../settings.dart'; import 'messagebubbledecorations.dart'; @@ -28,6 +32,7 @@ class MessageBubbleState extends State { var prettyDate = ""; var borderRadiousEh = 15.0; // var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString(); + var showClickableLinks = Provider.of(context).isExperimentEnabled(ClickableLinksExperiment); DateTime messageDate = Provider.of(context).timestamp; prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal()); @@ -45,16 +50,37 @@ class MessageBubbleState extends State { var wdgSender = SelectableText(senderDisplayStr, style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor())); - var wdgMessage = SelectableText( - widget.content + '\u202F', - //key: Key(myKey), - focusNode: _focus, - style: TextStyle( - color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), - ), - textAlign: TextAlign.left, - textWidthBasis: TextWidthBasis.longestLine, - ); + var wdgMessage; + + if (!showClickableLinks) { + wdgMessage = SelectableText( + widget.content + '\u202F', + //key: Key(myKey), + focusNode: _focus, + style: TextStyle( + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(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), + linkifiers: [UrlLinkifier()], // TODO: double-check on this (only web links to avoid Android messiness) + onOpen: (link) { + _modalOpenLink(context, link); + }, + //key: Key(myKey), + focusNode: _focus, + style: TextStyle( + color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), + ), + textAlign: TextAlign.left, + textWidthBasis: TextWidthBasis.longestLine, + ); + } var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); @@ -90,4 +116,58 @@ class MessageBubbleState extends State { children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); }); } + + void _modalOpenLink(BuildContext ctx, LinkableElement link) { + showModalBottomSheet( + context: ctx, + builder: (BuildContext bcontext) { + return Container( + // TODO: Ask re: hard-coded height + 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: [ + 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?" + ), + // TODO: Ask about styling preferences (should this be a reusable "inline-button"?) + Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: [ + 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)); + + // TODO: Ask about desired SnackBar + modal behaviour + final snackBar = SnackBar( + content: Text(AppLocalizations.of(context)!.copiedClipboardNotification), + ); + 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); + } else { + throw 'Could not launch $link'; + } + }, + ), + ), + ]), + ], + )), + )); + }); + } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d6814f42..c7621f85 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import package_info_plus_macos import path_provider_macos +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index e53880d9..e792cbf8 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -4,11 +4,14 @@ PODS: - FlutterMacOS - path_provider_macos (0.0.1): - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - 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`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: FlutterMacOS: @@ -17,12 +20,15 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos path_provider_macos: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b + url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4 PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c -COCOAPODS: 1.9.3 +COCOAPODS: 1.11.2 diff --git a/pubspec.lock b/pubspec.lock index 41cdef38..edb8e3de 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,7 +35,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -125,6 +125,13 @@ packages: description: flutter source: sdk 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: dependency: "direct main" description: flutter @@ -189,6 +196,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" + linkify: + dependency: transitive + description: + name: linkify + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" matcher: dependency: transitive description: @@ -411,6 +425,48 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: @@ -447,5 +503,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.13.0 <3.0.0" - flutter: ">=2.0.0" + dart: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index 366d60a6..68a1e5e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 file_picker: ^4.0.1 file_picker_desktop: ^1.1.0 + flutter_linkify: ^5.0.2 + url_launcher: ^6.0.12 dev_dependencies: msix: ^2.1.3 From 35dcc24e663682d2abb1d982626e7f772b0aaa25 Mon Sep 17 00:00:00 2001 From: Nima Boscarino Date: Sat, 6 Nov 2021 10:53:42 -0700 Subject: [PATCH 02/28] Remove resolved TODO statements, and destroy modal after copying link --- lib/widgets/messagebubble.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 39a8f9a3..128b9fab 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -68,7 +68,7 @@ class MessageBubbleState extends State { text: widget.content + '\u202F', // TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler? options: LinkifyOptions(humanize: false), - linkifiers: [UrlLinkifier()], // TODO: double-check on this (only web links to avoid Android messiness) + linkifiers: [UrlLinkifier()], onOpen: (link) { _modalOpenLink(context, link); }, @@ -122,7 +122,6 @@ class MessageBubbleState extends State { context: ctx, builder: (BuildContext bcontext) { return Container( - // TODO: Ask re: hard-coded height height: 200, // bespoke value courtesy of the [TextField] docs child: Center( child: Padding( @@ -134,7 +133,6 @@ class MessageBubbleState extends State { 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?" ), - // TODO: Ask about styling preferences (should this be a reusable "inline-button"?) Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10), @@ -143,10 +141,11 @@ class MessageBubbleState extends State { onPressed: () { Clipboard.setData(new ClipboardData(text: link.url)); - // TODO: Ask about desired SnackBar + modal behaviour final snackBar = SnackBar( content: Text(AppLocalizations.of(context)!.copiedClipboardNotification), ); + + Navigator.pop(bcontext); ScaffoldMessenger.of(context).showSnackBar(snackBar); }, ), From a58e09dec5f71f6d12a50238f110f019b651f2bb Mon Sep 17 00:00:00 2001 From: Nima Boscarino Date: Sun, 7 Nov 2021 00:56:48 -0700 Subject: [PATCH 03/28] android config for url_launcher --- android/app/src/main/AndroidManifest.xml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d562b149..ff6890c9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,11 +30,6 @@ - - - - - @@ -53,4 +48,11 @@ + + + + + + + From 1c03fdf1db794e16675e28e0c2e45e707edc106b Mon Sep 17 00:00:00 2001 From: Nima Boscarino Date: Mon, 8 Nov 2021 12:57:55 -0800 Subject: [PATCH 04/28] Add link styling, using main text colour --- lib/widgets/messagebubble.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 128b9fab..a91378b0 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -77,6 +77,9 @@ class MessageBubbleState extends State { style: TextStyle( color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(), ), + linkStyle: TextStyle( + color: Provider.of(context).current().mainTextColor(), + ), textAlign: TextAlign.left, textWidthBasis: TextWidthBasis.longestLine, ); From 51d98b517171d348cdd8b6bef3818ff026da1ebc Mon Sep 17 00:00:00 2001 From: RuLang Date: Wed, 10 Nov 2021 19:30:31 +0000 Subject: [PATCH 05/28] Update Russian Localization --- lib/l10n/intl_ru.arb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 66419265..b637956a 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -142,7 +142,7 @@ "addNewItem": "Добавить новый элемент в список", "todoPlaceholder": "Выполняю...", "newConnectionPaneTitle": "Новое соединение", - "networkStatusOnline": "Online", + "networkStatusOnline": "В сети", "networkStatusAttemptingTor": "Попытка подключиться к сети Tor", "networkStatusDisconnected": "Нет сети. Проверьте подключение к интернету", "viewGroupMembershipTooltip": "Просмотр членства в группе", @@ -188,20 +188,20 @@ "noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы", "radioNoPassword": "Незашифрованный (без пароля)", "radioUsePassword": "Пароль", - "copiedToClipboardNotification": "Copied to Clipboard", - "copyBtn": "Copy", + "copiedToClipboardNotification": "Скопировано в буфер обмена", + "copyBtn": "Копировать", "editProfile": "Изменить профиль", "newProfile": "Новый профиль", - "defaultProfileName": "Alice", + "defaultProfileName": "Алиса", "profileName": "Отображаемое имя", "editProfileTitle": "Изменить профиль", "addProfileTitle": "Добавить новый профиль", - "deleteBtn": "Delete", - "saveBtn": "Save", + "deleteBtn": "Удалить", + "saveBtn": "Сохранить", "displayNameLabel": "Отображаемое имя", "addressLabel": "Адрес", "puzzleGameBtn": "Puzzle Game", - "bulletinsBtn": "Bulletins", + "bulletinsBtn": "Бюллетень", "listsBtn": "Списки", "chatBtn": "Чат", "rejectGroupBtn": "Отклонить", @@ -219,14 +219,14 @@ "update": "Обновить", "inviteBtn": "Пригласить", "inviteToGroupLabel": "Пригласить в группу", - "groupNameLabel": "Group name", + "groupNameLabel": "Имя группы", "viewServerInfo": "Информация о сервере", "serverSynced": "Синхронизировано", "serverConnectivityDisconnected": "Сервер отключен", "serverConnectivityConnected": "Сервер подключен", "serverInfo": "Информация о сервере", "invitationLabel": "Приглашение", - "serverLabel": "Server", + "serverLabel": "Сервер", "search": "Поиск...", "cycleColoursDesktop": "Нажмите, чтобы переключать цвета.\nПравый клик чтобы сбросить.", "cycleColoursAndroid": "Нажмите, чтобы переключать цвета.\nНажмите и удерживайте, чтобы сбросить.", From b34ffcd2116d652196f5eec6c9d7946b4d729831 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 16 Nov 2021 15:03:18 -0800 Subject: [PATCH 06/28] update README with windows run in place instructions --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29280794..a9a1bb04 100644 --- a/README.md +++ b/README.md @@ -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). 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. @@ -53,6 +53,9 @@ To build a release version and load normal profiles, use `build-release.sh X` in - run `fetch-tor-win.ps1` to fetch Tor for windows - optional: launch cwtch-ui directly by running `flutter run -d 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) From 1d024ac63ed506a47e942fc8fa47a329fa426b93 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 16 Nov 2021 15:08:09 -0800 Subject: [PATCH 07/28] fix mac run instructions and make clear when running debug or release builds --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a9a1bb04..755398a4 100644 --- a/README.md +++ b/README.md @@ -42,16 +42,16 @@ To build a release version and load normal profiles, use `build-release.sh X` in - set `LD_LIBRARY_PATH="$PWD/linux"` - 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 -- 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` -- 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` ### Building on Windows (for Windows) - copy `libCwtch.dll` to `windows/`, or run `fetch-libcwtch-go.ps1` to download it - 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` - optional: to run the release build: - `cp windows/libCwtch.dll .` @@ -68,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 - run `fetch-tor-macos.sh` to fetch Tor or Download and install Tor Browser and `cp -r /Applications/Tor\ Browser.app/Contents/MacOS/Tor ./macos/` - `flutter build macos` -- optional: launch cwtch-ui build with `./build/linux/x64/release/bundle/cwtch` -- `./macos/package-release.sh` - -results in a Cwtch.dmg that has libCwtch.dylib and tor in it as well and can be installed into Applications +- optional: launch cwtch-ui release build with `./build/macos/Build/Products/Release/Cwtch.app/Contents/MacOS/Cwtch` +- 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 ### Known Platform Issues From 883b739b5c6d9b85f30c4a3d3e9a81cd1e7d4211 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 16 Nov 2021 16:34:28 -0800 Subject: [PATCH 08/28] bumping docker containers to flutter stable 2.5.3 --- .drone.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.drone.yml b/.drone.yml index 70b5c84e..7079ca5e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,7 +8,7 @@ clone: steps: - name: clone - image: cirrusci/flutter:dev + image: cirrusci/flutter:2.5.3 environment: buildbot_key_b64: from_secret: buildbot_key_b64 @@ -24,7 +24,7 @@ steps: - git checkout $DRONE_COMMIT - name: fetch - image: cirrusci/flutter:2.5.0-6.0.pre + image: cirrusci/flutter:2.5.3 volumes: - name: deps path: /root/.pub-cache @@ -47,7 +47,7 @@ steps: # #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting - name: build-linux - image: openpriv/flutter-desktop:linux-fdev2.5rc + image: openpriv/flutter-desktop:linux-fstable-2.5.3 volumes: - name: deps path: /root/.pub-cache @@ -61,7 +61,7 @@ steps: - rm -r cwtch - name: test-build-android - image: cirrusci/flutter:2.5.0-6.0.pre + image: cirrusci/flutter:2.5.3 when: event: pull_request volumes: @@ -71,7 +71,7 @@ steps: - flutter build apk --debug - name: build-android - image: cirrusci/flutter:2.5.0-6.0.pre + image: cirrusci/flutter:2.5.3 when: event: push environment: @@ -95,7 +95,7 @@ steps: #- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android - name: widget-tests - image: cirrusci/flutter:2.5.0-6.0.pre + image: cirrusci/flutter:2.5.3 volumes: - name: deps path: /root/.pub-cache @@ -174,7 +174,7 @@ clone: steps: - name: clone - image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc + image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 environment: buildbot_key_b64: from_secret: buildbot_key_b64 @@ -192,7 +192,7 @@ steps: - git checkout $Env:DRONE_COMMIT - name: fetch - image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc + image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 commands: - powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip" - powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }" @@ -201,7 +201,7 @@ steps: - .\fetch-libcwtch-go.ps1 - name: build-windows - image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc + image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 commands: - flutter pub get - $Env:version += type .\VERSION @@ -257,7 +257,7 @@ steps: - move *.sha512 deploy\$Env:builddir - name: deploy-windows - image: openpriv/flutter-desktop:windows-sdk30-fdev2.5rc + image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3 when: event: push status: [ success ] From 1d6b533df36df6e62b181010a76fc99afbadacea Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 18 Nov 2021 15:44:54 -0800 Subject: [PATCH 09/28] New Cwtch Library Integration --- lib/cwtch/cwtch.dart | 37 +++-- lib/cwtch/cwtchNotifier.dart | 65 +++++---- lib/cwtch/ffi.dart | 187 ++++++++++--------------- lib/cwtch/gomobile.dart | 33 ++--- lib/model.dart | 32 +++-- lib/models/message.dart | 24 ++-- lib/models/messages/filemessage.dart | 2 +- lib/models/messages/invitemessage.dart | 6 +- lib/models/messages/quotedmessage.dart | 6 +- lib/models/messages/textmessage.dart | 2 +- lib/views/contactsview.dart | 4 +- lib/views/doublecolview.dart | 4 +- lib/views/groupsettingsview.dart | 8 +- lib/views/messageview.dart | 12 +- lib/views/peersettingsview.dart | 4 +- lib/widgets/contactrow.dart | 8 +- lib/widgets/filebubble.dart | 26 +++- lib/widgets/invitationbubble.dart | 8 +- lib/widgets/messagebubble.dart | 2 +- lib/widgets/messagelist.dart | 4 +- lib/widgets/messagerow.dart | 8 +- lib/widgets/quotedmessage.dart | 2 +- 22 files changed, 235 insertions(+), 249 deletions(-) diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index c7efdbf3..cf42d0e0 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -29,36 +29,35 @@ abstract class Cwtch { void SendAppEvent(String jsonEvent); // ignore: non_constant_identifier_names - void AcceptContact(String profileOnion, String contactHandle); + void AcceptContact(String profileOnion, int contactHandle); // ignore: non_constant_identifier_names - void BlockContact(String profileOnion, String contactHandle); + void BlockContact(String profileOnion, int contactHandle); // ignore: non_constant_identifier_names - Future GetMessage(String profile, String handle, int index); + Future GetMessage(String profile, int handle, int index); // ignore: non_constant_identifier_names - Future 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); + Future GetMessageByContentHash(String profile, int handle, String contentHash); // ignore: non_constant_identifier_names - void ShareFile(String profile, String handle, String filepath); + void SendMessage(String profile, int handle, String message); // ignore: non_constant_identifier_names - void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey); + void SendInvitation(String profile, int handle, int target); + // ignore: non_constant_identifier_names - void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey); + 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 void CheckDownloadStatus(String profile, String fileKey); // 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 - void ArchiveConversation(String profile, String handle); + void ArchiveConversation(String profile, int handle); // ignore: non_constant_identifier_names - void DeleteContact(String profile, String handle); + void DeleteContact(String profile, int handle); // ignore: non_constant_identifier_names void CreateGroup(String profile, String server, String groupName); @@ -66,13 +65,11 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void ImportBundle(String profile, String bundle); // 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); + void RejectInvite(String profileOnion, int groupHandle); // ignore: non_constant_identifier_names void SetProfileAttribute(String profile, String key, String val); // 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); // ignore: non_constant_identifier_names void LoadServers(String password); diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 7f16ab49..5a3f9b6a 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -34,6 +34,7 @@ class CwtchNotifier { } void handleMessage(String type, dynamic data) { + EnvironmentConfig.debugLog("NewEvent $type $data"); switch (type) { case "CwtchStarted": appState.SetCwtchInit(); @@ -42,12 +43,15 @@ class CwtchNotifier { appState.SetAppError(data["Error"]); break; 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... profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword"); break; - case "PeerCreated": + case "ContactCreated": + EnvironmentConfig.debugLog("NewServer $data"); profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState( data["ProfileOnion"], + data["ConversationID"], data["RemotePeer"], nickname: data["nick"], status: data["status"], @@ -88,7 +92,7 @@ class CwtchNotifier { status = serverInfoState.status; } 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, imagePath: data["PicturePath"], nickname: data["GroupName"], @@ -111,13 +115,13 @@ class CwtchNotifier { } break; case "DeleteContact": - profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]); break; case "DeleteGroup": - profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["ConversationID"]); break; 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 (data["ConnectionState"] != null) { contact.status = data["ConnectionState"]; @@ -131,19 +135,20 @@ class CwtchNotifier { break; case "NewMessageFromPeer": notificationManager.notify("New Message From Peer!"); - if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++; + var identifier = int.parse(data["ConversationID"]); + if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++; } else { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.newMarker++; + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++; - profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now()); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; + profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); // 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(data["RemotePeer"])!.isOnline() == false) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.status = "Authenticated"; + if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) { + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated"; } break; @@ -151,10 +156,13 @@ class CwtchNotifier { // We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end... break; case "IndexedAcknowledgement": - var idx = data["Index"]; + var messageID = data["Index"]; + var identifier = int.parse(data["ConversationID"]); + var idx = identifier.toString() + messageID; + // We return -1 for protocol message acks if there is no message if (idx == "-1") break; - var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); + var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.getMessageKey(idx); if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); @@ -162,8 +170,8 @@ class CwtchNotifier { // 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 // end during syncing. - if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.isOnline() == false) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.status = "Authenticated"; + if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) { + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated"; } message.ackd = true; } catch (e) { @@ -172,19 +180,20 @@ class CwtchNotifier { } break; case "NewMessageFromGroup": + var identifier = int.parse(data["ConversationID"]); if (data["ProfileOnion"] != data["RemotePeer"]) { var idx = int.parse(data["Index"]); - var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages; + 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... if (currentTotal != null && idx >= currentTotal) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages = idx + 1; + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages = idx + 1; //if not currently open - if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++; + if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++; } else { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.newMarker++; + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } var timestampSent = DateTime.tryParse(data['TimestampSent'])!; @@ -204,7 +213,7 @@ class CwtchNotifier { } else { // from me (already displayed - do not update counter) var idx = data["Signature"]; - var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])?.getMessageKey(idx); + var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.getMessageKey(idx); if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); @@ -219,8 +228,8 @@ class CwtchNotifier { 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; + if (total != profileCN.getProfile(data["Identity"])?.contactList.findContact(contactHandle)!.totalMessages) { + profileCN.getProfile(data["Identity"])?.contactList.findContact(contactHandle)!.totalMessages = total; } break; case "SendMessageToPeerError": @@ -228,7 +237,7 @@ class CwtchNotifier { break; case "IndexedFailure": var idx = data["Index"]; - var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx); + var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])?.getMessageKey(idx); try { var message = Provider.of(key!.currentContext!, listen: false); message.error = true; @@ -240,7 +249,7 @@ class CwtchNotifier { // 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); + var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["GroupID"])!.getMessageKey(idx); if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); @@ -298,8 +307,8 @@ class CwtchNotifier { status = serverInfoState.status; } - if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"], + if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(groupInvite["GroupID"]) == null) { + profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["ConversationID"], groupInvite["GroupID"], authorization: ContactAuthorization.approved, imagePath: data["PicturePath"], nickname: groupInvite["GroupName"], diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 87eb66d1..14cdffe9 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -36,8 +36,9 @@ typedef VoidFromStringStringStringFn = void Function(Pointer, int, Pointer typedef void_from_string_string_string_string_function = Void Function(Pointer, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32); typedef VoidFromStringStringStringStringFn = void Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int); -typedef void_from_string_string_string_string_string_function = Void Function(Pointer, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32); -typedef VoidFromStringStringStringStringStringFn = void Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int); +// DownloadFile +typedef void_from_string_int_string_string_string_function = Void Function(Pointer, Int32, Int32, Pointer, Int32, Pointer, Int32, Pointer, Int32); +typedef VoidFromStringIntStringStringStringFn = void Function(Pointer, int, int, Pointer, int, Pointer, int, Pointer, int); typedef void_from_string_string_int_int_function = Void Function(Pointer, Int32, Pointer, Int32, Int64, Int64); typedef VoidFromStringStringIntIntFn = void Function(Pointer, int, Pointer, int, int, int); @@ -51,6 +52,9 @@ typedef StringFn = void Function(Pointer dir, int); typedef string_string_to_void_function = Void Function(Pointer str, Int32 length, Pointer str2, Int32 length2); typedef StringStringFn = void Function(Pointer, int, Pointer, int); +typedef string_int_to_void_function = Void Function(Pointer str, Int32 length, Int32 handle); +typedef VoidFromStringIntFn = void Function(Pointer, int, int); + typedef get_json_blob_string_function = Pointer Function(Pointer str, Int32 length); typedef GetJsonBlobStringFn = Pointer Function(Pointer str, int len); @@ -58,10 +62,26 @@ typedef GetJsonBlobStringFn = Pointer Function(Pointer str, int len) typedef get_json_blob_from_str_str_int_function = Pointer Function(Pointer, Int32, Pointer, Int32, Int32); typedef GetJsonBlobFromStrStrIntFn = Pointer Function(Pointer, int, Pointer, int, int); +typedef get_json_blob_from_str_int_int_function = Pointer Function(Pointer, Int32, Int32, Int32); +typedef GetJsonBlobFromStrIntIntFn = Pointer Function(Pointer, int, int, int); + +typedef get_json_blob_from_str_int_string_function = Pointer Function(Pointer, Int32, Int32, Pointer, Int32); +typedef GetJsonBlobFromStrIntStringFn = Pointer Function(Pointer, int, int, Pointer, 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 typedef get_json_blob_from_str_str_str_function = Pointer Function(Pointer, Int32, Pointer, Int32, Pointer, Int32); typedef GetJsonBlobFromStrStrStrFn = Pointer Function(Pointer, int, Pointer, int, Pointer, int); +typedef void_from_string_int_string_function = Void Function(Pointer, Int32, Int32, Pointer, Int32); +typedef VoidFromStringIntStringFn = void Function(Pointer, int, int, Pointer, int); + +typedef void_from_string_int_string_string_function = Void Function(Pointer, Int32, Int32, Pointer, Int32, Pointer, Int32); +typedef VoidFromStringIntStringStringFn = void Function(Pointer, int, int, Pointer, int, Pointer, int); + +typedef void_from_string_int_int_function =Void Function(Pointer, Int32, Int32, Int32); +typedef VoidFromStringIntIntFn = void Function(Pointer, int, int, int); + + typedef appbus_events_function = Pointer Function(); typedef AppbusEventsFn = Pointer Function(); @@ -266,17 +286,15 @@ class CwtchFfi implements Cwtch { } // ignore: non_constant_identifier_names - Future GetMessage(String profile, String handle, int index) async { - var getMessageC = library.lookup>("c_GetMessage"); + Future GetMessage(String profile, int handle, int index) async { + var getMessageC = library.lookup>("c_GetMessage"); // ignore: non_constant_identifier_names - final GetMessage = getMessageC.asFunction(); + final GetMessage = getMessageC.asFunction(); final utf8profile = profile.toNativeUtf8(); - final utf8handle = handle.toNativeUtf8(); - Pointer jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index); + Pointer jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index); String jsonMessage = jsonMessageBytes.toDartString(); _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); malloc.free(utf8profile); - malloc.free(utf8handle); return jsonMessage; } @@ -306,89 +324,75 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names - void AcceptContact(String profileOnion, String contactHandle) { - var acceptContact = library.lookup>("c_AcceptContact"); + void AcceptContact(String profileOnion, int contactHandle) { + var acceptContact = library.lookup>("c_AcceptContact"); // ignore: non_constant_identifier_names - final AcceptContact = acceptContact.asFunction(); + final AcceptContact = acceptContact.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = contactHandle.toNativeUtf8(); - AcceptContact(u1, u1.length, u2, u2.length); + AcceptContact(u1, u1.length, contactHandle); malloc.free(u1); - malloc.free(u2); } @override // ignore: non_constant_identifier_names - void BlockContact(String profileOnion, String contactHandle) { - var blockContact = library.lookup>("c_BlockContact"); + void BlockContact(String profileOnion, int contactHandle) { + var blockContact = library.lookup>("c_BlockContact"); // ignore: non_constant_identifier_names - final BlockContact = blockContact.asFunction(); + final BlockContact = blockContact.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = contactHandle.toNativeUtf8(); - BlockContact(u1, u1.length, u2, u2.length); + BlockContact(u1, u1.length, contactHandle); malloc.free(u1); - malloc.free(u2); } @override // ignore: non_constant_identifier_names - void SendMessage(String profileOnion, String contactHandle, String message) { - var sendMessage = library.lookup>("c_SendMessage"); + void SendMessage(String profileOnion, int contactHandle, String message) { + var sendMessage = library.lookup>("c_SendMessage"); // ignore: non_constant_identifier_names - final SendMessage = sendMessage.asFunction(); + final SendMessage = sendMessage.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = contactHandle.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(u2); malloc.free(u3); } @override // ignore: non_constant_identifier_names - void SendInvitation(String profileOnion, String contactHandle, String target) { - var sendInvitation = library.lookup>("c_SendInvitation"); + void SendInvitation(String profileOnion, int contactHandle, int target) { + var sendInvitation = library.lookup>("c_SendInvitation"); // ignore: non_constant_identifier_names - final SendInvitation = sendInvitation.asFunction(); + final SendInvitation = sendInvitation.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = contactHandle.toNativeUtf8(); - final u3 = target.toNativeUtf8(); - SendInvitation(u1, u1.length, u2, u2.length, u3, u3.length); + SendInvitation(u1, u1.length, contactHandle, target); malloc.free(u1); - malloc.free(u2); - malloc.free(u3); } @override // ignore: non_constant_identifier_names - void ShareFile(String profileOnion, String contactHandle, String filepath) { - var shareFile = library.lookup>("c_ShareFile"); + void ShareFile(String profileOnion, int contactHandle, String filepath) { + var shareFile = library.lookup>("c_ShareFile"); // ignore: non_constant_identifier_names - final ShareFile = shareFile.asFunction(); + final ShareFile = shareFile.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = contactHandle.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(u2); malloc.free(u3); } @override // ignore: non_constant_identifier_names - void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) { - var dlFile = library.lookup>("c_DownloadFile"); + void DownloadFile(String profileOnion, int contactHandle, String filepath, String manifestpath, String filekey) { + var dlFile = library.lookup>("c_DownloadFile"); // ignore: non_constant_identifier_names - final DownloadFile = dlFile.asFunction(); + final DownloadFile = dlFile.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = contactHandle.toNativeUtf8(); final u3 = filepath.toNativeUtf8(); final u4 = manifestpath.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(u2); malloc.free(u3); malloc.free(u4); malloc.free(u5); @@ -396,7 +400,7 @@ class CwtchFfi implements Cwtch { @override // 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 } @@ -415,16 +419,14 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names - void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { - var fn = library.lookup>("c_VerifyOrResumeDownload"); + void VerifyOrResumeDownload(String profileOnion, int contactHandle, String filekey) { + var fn = library.lookup>("c_VerifyOrResumeDownload"); // ignore: non_constant_identifier_names - final VerifyOrResumeDownload = fn.asFunction(); + final VerifyOrResumeDownload = fn.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = contactHandle.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(u2); malloc.free(u3); } @@ -451,34 +453,16 @@ class CwtchFfi implements Cwtch { malloc.free(u2); } - @override - // ignore: non_constant_identifier_names - void SetGroupAttribute(String profileOnion, String groupHandle, String key, String value) { - var setGroupAttribute = library.lookup>("c_SetGroupAttribute"); - // ignore: non_constant_identifier_names - final SetGroupAttribute = setGroupAttribute.asFunction(); - 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>("c_RejectInvite"); + void RejectInvite(String profileOnion, int groupHandle) { + var rejectInvite = library.lookup>("c_RejectInvite"); // ignore: non_constant_identifier_names - final RejectInvite = rejectInvite.asFunction(); + final RejectInvite = rejectInvite.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = groupHandle.toNativeUtf8(); - RejectInvite(u1, u1.length, u2, u2.length); + RejectInvite(u1, u1.length, groupHandle); malloc.free(u1); - malloc.free(u2); } @override @@ -499,42 +483,27 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names - void ArchiveConversation(String profileOnion, String handle) { - var archiveConversation = library.lookup>("c_ArchiveConversation"); + void ArchiveConversation(String profileOnion, int handle) { + var archiveConversation = library.lookup>("c_ArchiveConversation"); // ignore: non_constant_identifier_names - final ArchiveConversation = archiveConversation.asFunction(); + final ArchiveConversation = archiveConversation.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = handle.toNativeUtf8(); - ArchiveConversation(u1, u1.length, u2, u2.length); + ArchiveConversation(u1, u1.length, handle); malloc.free(u1); - malloc.free(u2); + } @override // ignore: non_constant_identifier_names - void DeleteContact(String profileOnion, String handle) { - var deleteContact = library.lookup>("c_DeleteContact"); + void DeleteContact(String profileOnion, int handle) { + var deleteContact = library.lookup>("c_DeleteContact"); // ignore: non_constant_identifier_names - final DeleteContact = deleteContact.asFunction(); + final DeleteContact = deleteContact.asFunction(); final u1 = profileOnion.toNativeUtf8(); - final u2 = handle.toNativeUtf8(); - DeleteContact(u1, u1.length, u2, u2.length); + DeleteContact(u1, u1.length, handle); 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>("c_UpdateMessageFlags"); - // ignore: non_constant_identifier_names - final updateMessageFlags = updateMessageFlagsC.asFunction(); - final utf8profile = profile.toNativeUtf8(); - final utf8handle = handle.toNativeUtf8(); - updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags); - malloc.free(utf8profile); - malloc.free(utf8handle); - } @override // ignore: non_constant_identifier_names @@ -566,17 +535,15 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names - void SetContactAttribute(String profile, String contact, String key, String val) { - var setContactAttribute = library.lookup>("c_SetContactAttribute"); + void SetConversationAttribute(String profile, int contact, String key, String val) { + var setContactAttribute = library.lookup>("c_SetContactAttribute"); // ignore: non_constant_identifier_names - final SetContactAttribute = setContactAttribute.asFunction(); + final SetContactAttribute = setContactAttribute.asFunction(); final u1 = profile.toNativeUtf8(); - final u2 = contact.toNativeUtf8(); final u3 = key.toNativeUtf8(); final u4 = key.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(u2); malloc.free(u3); malloc.free(u4); } @@ -703,19 +670,17 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names - Future GetMessageByContentHash(String profile, String handle, String contentHash) async { - var getMessagesByContentHashC = library.lookup>("c_GetMessagesByContentHash"); + Future GetMessageByContentHash(String profile, int handle, String contentHash) async { + var getMessagesByContentHashC = library.lookup>("c_GetMessagesByContentHash"); // ignore: non_constant_identifier_names - final GetMessagesByContentHash = getMessagesByContentHashC.asFunction(); + final GetMessagesByContentHash = getMessagesByContentHashC.asFunction(); final utf8profile = profile.toNativeUtf8(); - final utf8handle = handle.toNativeUtf8(); final utf8contentHash = contentHash.toNativeUtf8(); - Pointer jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length); + Pointer jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, handle, utf8contentHash, utf8contentHash.length); String jsonMessage = jsonMessageBytes.toDartString(); _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); malloc.free(utf8profile); - malloc.free(utf8handle); malloc.free(utf8contentHash); return jsonMessage; } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index ae30a80c..06c91f74 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -87,7 +87,7 @@ class CwtchGomobile implements Cwtch { } // ignore: non_constant_identifier_names - Future GetMessage(String profile, String handle, int index) { + Future GetMessage(String profile, int handle, int index) { print("gomobile.dart GetMessage " + index.toString()); return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index}); } @@ -109,42 +109,42 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void AcceptContact(String profileOnion, String contactHandle) { + void AcceptContact(String profileOnion, int contactHandle) { cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); } @override // ignore: non_constant_identifier_names - void BlockContact(String profileOnion, String contactHandle) { + void BlockContact(String profileOnion, int contactHandle) { cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); } @override // ignore: non_constant_identifier_names - void SendMessage(String profileOnion, String contactHandle, String message) { + void SendMessage(String profileOnion, int contactHandle, String message) { cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "handle": contactHandle, "message": message}); } @override // ignore: non_constant_identifier_names - void SendInvitation(String profileOnion, String contactHandle, String target) { + void SendInvitation(String profileOnion, int contactHandle, int target) { cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target}); } @override // ignore: non_constant_identifier_names - void ShareFile(String profileOnion, String contactHandle, String filepath) { + void ShareFile(String profileOnion, int contactHandle, String filepath) { cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath}); } @override // 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) { cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey}); } // 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) { cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey}); } @@ -156,7 +156,7 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { + void VerifyOrResumeDownload(String profileOnion, int contactHandle, String filekey) { cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "handle": contactHandle, "filekey": filekey}); } @@ -180,7 +180,7 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void RejectInvite(String profileOnion, String groupHandle) { + void RejectInvite(String profileOnion, int groupHandle) { cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle}); } @@ -191,21 +191,16 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void DeleteContact(String profileOnion, String handle) { + void DeleteContact(String profileOnion, int handle) { cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "handle": handle}); } @override // ignore: non_constant_identifier_names - void ArchiveConversation(String profileOnion, String contactHandle) { + void ArchiveConversation(String profileOnion, int contactHandle) { cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "handle": contactHandle}); } - @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 // ignore: non_constant_identifier_names @@ -215,7 +210,7 @@ class CwtchGomobile implements Cwtch { @override // 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) { cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "Contact": contact, "Key": key, "Val": val}); } @@ -280,7 +275,7 @@ class CwtchGomobile implements Cwtch { } @override - Future GetMessageByContentHash(String profile, String handle, String contentHash) { + Future GetMessageByContentHash(String profile, int handle, String contentHash) { return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "contentHash": contentHash}); } } diff --git a/lib/model.dart b/lib/model.dart index 23780557..a0882561 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -29,7 +29,7 @@ class AppState extends ChangeNotifier { bool cwtchIsClosing = false; String appError = ""; String? _selectedProfile; - String? _selectedConversation; + int? _selectedConversation; int _initialScrollIndex = 0; int _hoveredIndex = -1; int? _selectedIndex; @@ -51,8 +51,8 @@ class AppState extends ChangeNotifier { notifyListeners(); } - String? get selectedConversation => _selectedConversation; - set selectedConversation(String? newVal) { + int? get selectedConversation => _selectedConversation; + set selectedConversation(int? newVal) { this._selectedConversation = newVal; notifyListeners(); } @@ -172,8 +172,8 @@ class ContactListState extends ChangeNotifier { //} } - void updateLastMessageTime(String forOnion, DateTime newMessageTime) { - var contact = getContact(forOnion); + void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) { + var contact = getContact(forIdentifier); if (contact == null) return; // Assert that the new time is after the current last message time AND that @@ -191,18 +191,24 @@ class ContactListState extends ChangeNotifier { List get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier - ContactInfoState? getContact(String onion) { - int idx = _contacts.indexWhere((element) => element.onion == onion); + ContactInfoState? getContact(int identifier) { + int idx = _contacts.indexWhere((element) => element.identifier == identifier); return idx >= 0 ? _contacts[idx] : null; } - void removeContact(String onion) { - int idx = _contacts.indexWhere((element) => element.onion == onion); + void removeContact(int identifier) { + int idx = _contacts.indexWhere((element) => element.identifier == identifier); if (idx >= 0) { _contacts.removeAt(idx); notifyListeners(); } } + + ContactInfoState? findContact(String byHandle) { + int idx = _contacts.indexWhere((element) => element.onion == byHandle); + return idx >= 0 ? _contacts[idx] : null; + } + } class ProfileInfoState extends ChangeNotifier { @@ -238,7 +244,7 @@ class ProfileInfoState extends ChangeNotifier { if (contactsJson != null && contactsJson != "" && contactsJson != "null") { List contacts = jsonDecode(contactsJson); this._contacts.addAll(contacts.map((contact) { - return ContactInfoState(this.onion, contact["onion"], + return ContactInfoState(this.onion, contact["identifier"], contact["onion"], nickname: contact["name"], status: contact["status"], imagePath: contact["picture"], @@ -254,7 +260,7 @@ class ProfileInfoState extends ChangeNotifier { // dummy set to invoke sort-on-load 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); } } @@ -341,6 +347,7 @@ class ProfileInfoState extends ChangeNotifier { } else { this._contacts.add(ContactInfoState( this.onion, + contact["identifier"], contact["onion"], nickname: contact["name"], status: contact["status"], @@ -496,6 +503,7 @@ ContactAuthorization stringToContactAuthorization(String authStr) { class ContactInfoState extends ChangeNotifier { final String profileOnion; + final int identifier; final String onion; late String _nickname; @@ -515,7 +523,7 @@ class ContactInfoState extends ChangeNotifier { String? _server; late bool _archived; - ContactInfoState(this.profileOnion, this.onion, + ContactInfoState(this.profileOnion, this.identifier, this.onion, {nickname = "", isGroup = false, authorization = ContactAuthorization.unknown, diff --git a/lib/models/message.dart b/lib/models/message.dart index 2156b8d6..062c60a8 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -31,11 +31,11 @@ abstract class Message { Widget getPreviewWidget(BuildContext context); } -Future messageHandler(BuildContext context, String profileOnion, String contactHandle, int index) { +Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index) { try { - var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); + var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index); return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - var metadata = MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true); + var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", null, 0, false, true); try { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); // There are 2 conditions in which this error condition can be met: @@ -50,23 +50,20 @@ Future messageHandler(BuildContext context, String profileOnion, String if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); + return messageHandler(context, profileOnion, conversationIdentifier, index).then((value) => value); }); } // Construct the initial metadata + var messageID = messageWrapper['ID']; var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; var senderHandle = messageWrapper['PeerID']; var senderImage = messageWrapper['ContactImage']; var flags = int.parse(messageWrapper['Flags'].toString()); var ackd = messageWrapper['Acknowledged']; var error = messageWrapper['Error'] != null; - String? signature; - // If this is a group, store the signature - if (contactHandle.length == GroupConversationHandleLength) { - signature = messageWrapper['Signature']; - } - metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error); + var signature = messageWrapper['Signature']; + metadata = MessageMetadata(profileOnion, conversationIdentifier, index, messageID, timestamp, senderHandle, senderImage, signature, flags, ackd, error); dynamic message = jsonDecode(messageWrapper['Message']); var content = message['d'] as dynamic; @@ -92,15 +89,16 @@ Future messageHandler(BuildContext context, String profileOnion, String } }); } catch (e) { - return Future.value(MalformedMessage(MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true))); + return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", null, 0, false, true))); } } class MessageMetadata extends ChangeNotifier { // meta-metadata final String profileOnion; - final String contactHandle; + final int conversationIdentifier; final int messageIndex; + final int messageID; final DateTime timestamp; final String senderHandle; @@ -129,5 +127,5 @@ class MessageMetadata extends ChangeNotifier { 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.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); } diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index 0ea5cbc7..24c6e152 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -21,7 +21,7 @@ class FileMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString(); + String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); dynamic shareObj = jsonDecode(this.content); if (shareObj == null) { return MessageRow(MalformedBubble()); diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index df21e313..fb58f083 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -21,14 +21,14 @@ class InviteMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString(); + String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); String inviteTarget; String inviteNick; String invite = this.content; if (this.content.length == TorV3ContactHandleLength) { inviteTarget = this.content; - var targetContact = Provider.of(context).contactList.getContact(inviteTarget); + var targetContact = Provider.of(context).contactList.findContact(inviteTarget); inviteNick = targetContact == null ? this.content : targetContact.nickname; } else { var parts = this.content.toString().split("||"); @@ -54,7 +54,7 @@ class InviteMessage extends Message { String invite = this.content; if (this.content.length == TorV3ContactHandleLength) { inviteTarget = this.content; - var targetContact = Provider.of(context).contactList.getContact(inviteTarget); + var targetContact = Provider.of(context).contactList.findContact(inviteTarget); inviteNick = targetContact == null ? this.content : targetContact.nickname; } else { var parts = this.content.toString().split("||"); diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 8bc0c905..6ddba3a3 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -70,7 +70,7 @@ class QuotedMessage extends Message { return MalformedMessage(this.metadata).getWidget(context); } - var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.contactHandle, message["quotedHash"]); + var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]); int messageIndex = metadata.messageIndex; Future quotedMessage = quotedMessagePotentials.then((matchingMessages) { if (matchingMessages == "[]") { @@ -94,11 +94,11 @@ class QuotedMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString(); + String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); return MessageRow( QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { 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); })), diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index a5782ac4..ac996152 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -32,7 +32,7 @@ class TextMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString(); + String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); return MessageRow(MessageBubble(this.content), key: Provider.of(bcontext).getMessageKey(idx)); }); } diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 895b06ac..bb9c585c 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -22,7 +22,7 @@ class ContactsView extends StatefulWidget { } // 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 var initialIndex = Provider.of(context, listen: false).contactList.getContact(handle)!.unreadMessages; Provider.of(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0; @@ -36,7 +36,7 @@ void selectConversation(BuildContext context, String handle) { if (Provider.of(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(context, listen: false).onion; Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/views/doublecolview.dart b/lib/views/doublecolview.dart index 99d2aa9f..329821c7 100644 --- a/lib/views/doublecolview.dart +++ b/lib/views/doublecolview.dart @@ -34,8 +34,8 @@ class _DoubleColumnViewState extends State { MultiProvider(providers: [ ChangeNotifierProvider.value(value: Provider.of(context)), ChangeNotifierProvider.value( - value: flwtch.selectedConversation != null ? Provider.of(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")), - ], child: Container(key: Key(flwtch.selectedConversation??"never_this"), child: MessageView())), + value: flwtch.selectedConversation != null ? Provider.of(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", -1, "")), + ], child: Container(key: Key(flwtch.selectedConversation!.toString()), child: MessageView())), ), ], ); diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 72afb5d2..83e57d32 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -78,9 +78,9 @@ class _GroupSettingsViewState extends State { readonly: false, onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).onion; + var handle = Provider.of(context, listen: false).identifier; Provider.of(context, listen: false).nickname = ctrlrNick.text; - Provider.of(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text); + Provider.of(context, listen: false).cwtch.SetConversationAttribute(profileOnion, handle, "local.name", ctrlrNick.text); // todo translations final snackBar = SnackBar(content: Text("Group Nickname changed successfully")); ScaffoldMessenger.of(context).showSnackBar(snackBar); @@ -140,7 +140,7 @@ class _GroupSettingsViewState extends State { child: ElevatedButton.icon( onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).onion; + var handle = Provider.of(context, listen: false).identifier; // locally update cache... Provider.of(context, listen: false).isArchived = true; Provider.of(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); @@ -195,7 +195,7 @@ class _GroupSettingsViewState extends State { child: Text(AppLocalizations.of(context)!.yesLeave), onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).onion; + var handle = Provider.of(context, listen: false).identifier; // locally update cache... Provider.of(context, listen: false).isArchived = true; Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, handle); diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index f35460fb..79ae41e3 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -33,7 +33,7 @@ class MessageView extends StatefulWidget { class _MessageViewState extends State { final ctrlrCompose = TextEditingController(); final focusNode = FocusNode(); - String selectedContact = ""; + int selectedContact = -1; ItemPositionsListener scrollListener = ItemPositionsListener.create(); ItemScrollController scrollController = ItemScrollController(); @@ -180,7 +180,7 @@ class _MessageViewState extends State { ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage); Provider.of(context, listen: false) .cwtch - .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); + .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).identifier, jsonEncode(cm)); } catch (e) {} Provider.of(context, listen: false).selectedIndex = null; _sendMessageHelper(); @@ -189,7 +189,7 @@ class _MessageViewState extends State { ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text); Provider.of(context, listen: false) .cwtch - .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, jsonEncode(cm)); + .SendMessage(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).identifier, jsonEncode(cm)); _sendMessageHelper(); } } @@ -198,14 +198,14 @@ class _MessageViewState extends State { void _sendInvitation([String? ignoredParam]) { Provider.of(context, listen: false) .cwtch - .SendInvitation(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, this.selectedContact); + .SendInvitation(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).identifier, this.selectedContact); _sendMessageHelper(); } void _sendFile(String filePath) { Provider.of(context, listen: false) .cwtch - .ShareFile(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion, filePath); + .ShareFile(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).identifier, filePath); _sendMessageHelper(); } @@ -216,7 +216,7 @@ class _MessageViewState extends State { Provider.of(context, listen: false).totalMessages++; Provider.of(context, listen: false).newMarker++; // Resort the contact list... - Provider.of(context, listen: false).contactList.updateLastMessageTime(Provider.of(context, listen: false).onion, DateTime.now()); + Provider.of(context, listen: false).contactList.updateLastMessageTime(Provider.of(context, listen: false).identifier, DateTime.now()); }); } diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 7b59a604..235ca8ca 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -200,7 +200,7 @@ class _PeerSettingsViewState extends State { child: ElevatedButton.icon( onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).onion; + var handle = Provider.of(context, listen: false).identifier; // locally update cache... Provider.of(context, listen: false).isArchived = true; Provider.of(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle); @@ -239,7 +239,7 @@ class _PeerSettingsViewState extends State { child: Text(AppLocalizations.of(context)!.yesLeave), onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).onion; + var handle = Provider.of(context, listen: false).identifier; // locally update cache... Provider.of(context, listen: false).isArchived = true; Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, handle); diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index 6a50008a..c34e4668 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -105,7 +105,7 @@ class _ContactRowState extends State { ), ]), onTap: () { - selectConversation(context, contact.onion); + selectConversation(context, contact.identifier); }, )); } @@ -113,16 +113,16 @@ class _ContactRowState extends State { void _btnApprove() { Provider.of(context, listen: false) .cwtch - .AcceptContact(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).onion); + .AcceptContact(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).identifier); } void _btnReject() { ContactInfoState contact = Provider.of(context, listen: false); if (contact.isGroup == true) { - Provider.of(context, listen: false).cwtch.RejectInvite(Provider.of(context, listen: false).profileOnion, contact.onion); + Provider.of(context, listen: false).cwtch.RejectInvite(Provider.of(context, listen: false).profileOnion, contact.identifier); Provider.of(context, listen: false).removeContact(contact.onion); } else { - Provider.of(context, listen: false).cwtch.BlockContact(Provider.of(context, listen: false).profileOnion, contact.onion); + Provider.of(context, listen: false).cwtch.BlockContact(Provider.of(context, listen: false).profileOnion, contact.identifier); } } diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 33fa503e..e3aea951 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -49,7 +49,7 @@ class FileBubbleState extends State { // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; if (!fromMe) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { @@ -152,9 +152,16 @@ class FileBubbleState extends State { if (Platform.isAndroid) { Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); - Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); + //Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); Provider.of(context, listen: false).flags |= 0x02; - Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey()); + ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); + if (contact != null) { + Provider + .of(context, listen: false) + .cwtch + .CreateDownloadableFile( + profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey()); + } } else { try { selectedFileName = await saveFile( @@ -165,9 +172,16 @@ class FileBubbleState extends State { print("saving to " + file.path); var manifestPath = file.path + ".manifest"; Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); - Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); + //Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); Provider.of(context, listen: false).flags |= 0x02; - Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); + ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); + if (contact != null) { + Provider + .of(context, listen: false) + .cwtch + .DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, + widget.fileKey()); + } } } catch (e) { print(e); @@ -177,7 +191,7 @@ class FileBubbleState extends State { void _btnResume() async { var profileOnion = Provider.of(context, listen: false).onion; - var handle = Provider.of(context, listen: false).senderHandle; + var handle = Provider.of(context, listen: false).conversationIdentifier; Provider.of(context, listen: false).downloadMarkResumed(widget.fileKey()); Provider.of(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey()); } diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 0e6b9219..e3452d91 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -36,7 +36,7 @@ class InvitationBubbleState extends State { Widget build(BuildContext context) { var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; var isGroup = widget.overlay == InviteGroupOverlay; - isAccepted = Provider.of(context).contactList.getContact(widget.inviteTarget) != null; + isAccepted = Provider.of(context).contactList.findContact(widget.inviteTarget) != null; var borderRadiousEh = 15.0; var showGroupInvite = Provider.of(context).isExperimentEnabled(TapirGroupsExperiment); rejected = Provider.of(context).flags & 0x01 == 0x01; @@ -45,7 +45,7 @@ class InvitationBubbleState extends State { // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; if (!fromMe) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { @@ -69,7 +69,7 @@ class InvitationBubbleState extends State { ? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) : fromMe ? senderInviteChrome( - AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of(context).contactList.getContact(widget.inviteTarget)!.nickname : widget.inviteTarget) + AppLocalizations.of(context)!.sendAnInvitation, isGroup ? Provider.of(context).contactList.findContact(widget.inviteTarget)!.nickname : widget.inviteTarget) : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget)); Widget wdgDecorations; @@ -130,7 +130,7 @@ class InvitationBubbleState extends State { var profileOnion = Provider.of(context, listen: false).onion; var contact = Provider.of(context, listen: false).onion; var idx = Provider.of(context, listen: false).messageIndex; - Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); + //Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); Provider.of(context, listen: false).flags |= 0x01; }); } diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index a91378b0..55e67bc2 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -40,7 +40,7 @@ class MessageBubbleState extends State { // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; if (!fromMe) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 33406b72..3954ea74 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -76,8 +76,8 @@ class _MessageListState extends State { reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... itemBuilder: (itemBuilderContext, index) { var profileOnion = Provider.of(outerContext, listen: false).onion; - var contactHandle = Provider.of(outerContext, listen: false).onion; - var messageIndex = Provider.of(outerContext).totalMessages - index - 1; + var contactHandle = Provider.of(outerContext, listen: false).identifier; + var messageIndex = index; return FutureBuilder( future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index d4789266..0e9420e0 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -48,8 +48,8 @@ class MessageRowState extends State with SingleTickerProviderStateMi @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; - var isContact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle) != null; - var isBlocked = isContact ? Provider.of(context).contactList.getContact(Provider.of(context).senderHandle)!.isBlocked : false; + var isContact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle) != null; + var isBlocked = isContact ? Provider.of(context).contactList.findContact(Provider.of(context).senderHandle)!.isBlocked : false; var actualMessage = Flexible(flex: 3, fit: FlexFit.loose, child: widget.child); _dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft; @@ -60,7 +60,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi var senderDisplayStr = ""; if (!fromMe) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { @@ -249,7 +249,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi } void _btnGoto() { - selectConversation(context, Provider.of(context, listen: false).senderHandle); + selectConversation(context, Provider.of(context, listen: false).conversationIdentifier); } void _btnAdd() { diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index 6c66c354..6d1854bc 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -34,7 +34,7 @@ class QuotedMessageBubbleState extends State { // If the sender is not us, then we want to give them a nickname... var senderDisplayStr = ""; if (!fromMe) { - ContactInfoState? contact = Provider.of(context).contactList.getContact(Provider.of(context).senderHandle); + ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { senderDisplayStr = contact.nickname; } else { From 880c1c107b75f29009f4c7d75c23aa01b472d0ba Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 25 Nov 2021 15:59:54 -0800 Subject: [PATCH 10/28] UI Updates to new Cwtch API --- lib/cwtch/cwtchNotifier.dart | 23 +- lib/cwtch/ffi.dart | 21 +- lib/cwtch/gomobile.dart | 3 +- lib/model.dart | 4 +- lib/models/message.dart | 3 +- lib/models/servers.dart | 9 +- lib/views/addeditprofileview.dart | 2 + lib/views/addeditservers.dart | 447 ++++++++++++++---------------- lib/views/contactsview.dart | 1 - lib/views/globalsettingsview.dart | 5 +- lib/views/messageview.dart | 8 +- lib/views/profilemgrview.dart | 8 +- lib/views/serversview.dart | 149 +++++----- lib/widgets/filebubble.dart | 22 +- lib/widgets/messagebubble.dart | 7 +- lib/widgets/messagerow.dart | 10 +- lib/widgets/serverrow.dart | 62 ++--- 17 files changed, 369 insertions(+), 415 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 5a3f9b6a..274026bf 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -23,7 +23,8 @@ class CwtchNotifier { late AppState appState; 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; settings = settingsCN; error = errorCN; @@ -51,7 +52,7 @@ class CwtchNotifier { EnvironmentConfig.debugLog("NewServer $data"); profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState( data["ProfileOnion"], - data["ConversationID"], + int.parse(data["ConversationID"]), data["RemotePeer"], nickname: data["nick"], status: data["status"], @@ -68,13 +69,7 @@ class CwtchNotifier { break; case "NewServer": EnvironmentConfig.debugLog("NewServer $data"); - serverListState.add( - data["Onion"], - data["ServerBundle"], - data["Running"] == "true", - data["Description"], - data["Autostart"] == "true", - data["StorageType"] == "storage-password"); + serverListState.add(data["Onion"], data["ServerBundle"], data["Running"] == "true", data["Description"], data["Autostart"] == "true", data["StorageType"] == "storage-password"); break; case "ServerIntentUpdate": EnvironmentConfig.debugLog("ServerIntentUpdate $data"); @@ -158,7 +153,7 @@ class CwtchNotifier { case "IndexedAcknowledgement": var messageID = data["Index"]; var identifier = int.parse(data["ConversationID"]); - var idx = identifier.toString() + messageID; + var idx = identifier.toString() + messageID; // We return -1 for protocol message acks if there is no message if (idx == "-1") break; @@ -207,7 +202,7 @@ class CwtchNotifier { // 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 `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!"); } } else { @@ -271,8 +266,8 @@ class CwtchNotifier { case "UpdateGlobalSettings": settings.handleUpdate(jsonDecode(data["Data"])); break; - case "SetAttribute": - if (data["Key"] == "public.name") { + case "UpdatedProfileAttribute": + if (data["Key"] == "public.profile.name") { profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"]; } else { EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}"); @@ -308,7 +303,7 @@ class CwtchNotifier { } if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(groupInvite["GroupID"]) == null) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["ConversationID"], groupInvite["GroupID"], + profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["ConversationID"], groupInvite["GroupID"], authorization: ContactAuthorization.approved, imagePath: data["PicturePath"], nickname: groupInvite["GroupName"], diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 14cdffe9..78be3e70 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -63,10 +63,16 @@ typedef get_json_blob_from_str_str_int_function = Pointer Function(Pointer typedef GetJsonBlobFromStrStrIntFn = Pointer Function(Pointer, int, Pointer, int, int); typedef get_json_blob_from_str_int_int_function = Pointer Function(Pointer, Int32, Int32, Int32); -typedef GetJsonBlobFromStrIntIntFn = Pointer Function(Pointer, int, int, int); +typedef GetJsonBlobFromStrIntIntFn = Pointer Function(Pointer, int, int, int); typedef get_json_blob_from_str_int_string_function = Pointer Function(Pointer, Int32, Int32, Pointer, Int32); -typedef GetJsonBlobFromStrIntStringFn = Pointer Function(Pointer, int, int, Pointer, int,); +typedef GetJsonBlobFromStrIntStringFn = Pointer Function( + Pointer, + int, + int, + Pointer, + 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 typedef get_json_blob_from_str_str_str_function = Pointer Function(Pointer, Int32, Pointer, Int32, Pointer, Int32); @@ -78,10 +84,9 @@ typedef VoidFromStringIntStringFn = void Function(Pointer, int, int, Point typedef void_from_string_int_string_string_function = Void Function(Pointer, Int32, Int32, Pointer, Int32, Pointer, Int32); typedef VoidFromStringIntStringStringFn = void Function(Pointer, int, int, Pointer, int, Pointer, int); -typedef void_from_string_int_int_function =Void Function(Pointer, Int32, Int32, Int32); +typedef void_from_string_int_int_function = Void Function(Pointer, Int32, Int32, Int32); typedef VoidFromStringIntIntFn = void Function(Pointer, int, int, int); - typedef appbus_events_function = Pointer Function(); typedef AppbusEventsFn = Pointer Function(); @@ -325,7 +330,7 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names void AcceptContact(String profileOnion, int contactHandle) { - var acceptContact = library.lookup>("c_AcceptContact"); + var acceptContact = library.lookup>("c_AcceptConversation"); // ignore: non_constant_identifier_names final AcceptContact = acceptContact.asFunction(); final u1 = profileOnion.toNativeUtf8(); @@ -430,7 +435,6 @@ class CwtchFfi implements Cwtch { malloc.free(u3); } - @override // ignore: non_constant_identifier_names void ResetTor() { @@ -453,7 +457,6 @@ class CwtchFfi implements Cwtch { malloc.free(u2); } - @override // ignore: non_constant_identifier_names void RejectInvite(String profileOnion, int groupHandle) { @@ -490,7 +493,6 @@ class CwtchFfi implements Cwtch { final u1 = profileOnion.toNativeUtf8(); ArchiveConversation(u1, u1.length, handle); malloc.free(u1); - } @override @@ -504,7 +506,6 @@ class CwtchFfi implements Cwtch { malloc.free(u1); } - @override // ignore: non_constant_identifier_names void DeleteProfile(String onion, String currentPassword) { @@ -526,7 +527,7 @@ class CwtchFfi implements Cwtch { final SetProfileAttribute = setProfileAttribute.asFunction(); final u1 = profile.toNativeUtf8(); final u2 = key.toNativeUtf8(); - final u3 = key.toNativeUtf8(); + final u3 = val.toNativeUtf8(); SetProfileAttribute(u1, u1.length, u2, u2.length, u3, u3.length); malloc.free(u1); malloc.free(u2); diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 06c91f74..f8d62f3a 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -201,7 +201,6 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "handle": contactHandle}); } - @override // ignore: non_constant_identifier_names void SetProfileAttribute(String profile, String key, String val) { @@ -268,7 +267,7 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val}); } - @override + @override Future Shutdown() async { print("gomobile.dart Shutdown"); cwtchPlatform.invokeMethod("Shutdown", {}); diff --git a/lib/model.dart b/lib/model.dart index a0882561..1526d190 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -208,7 +208,6 @@ class ContactListState extends ChangeNotifier { int idx = _contacts.indexWhere((element) => element.onion == byHandle); return idx >= 0 ? _contacts[idx] : null; } - } class ProfileInfoState extends ChangeNotifier { @@ -601,7 +600,7 @@ class ContactInfoState extends ChangeNotifier { if (newVal > 0) { this._newMarker = newVal; } else { - this._newMarkerClearAt = DateTime.now().add(const Duration(minutes:2)); + this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2)); } this._unreadMessages = newVal; notifyListeners(); @@ -616,6 +615,7 @@ class ContactInfoState extends ChangeNotifier { } return this._newMarker; } + // what's a getter that sometimes sets without a setter // that sometimes doesn't set set newMarker(int newVal) { diff --git a/lib/models/message.dart b/lib/models/message.dart index 062c60a8..708cd8af 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -127,5 +127,6 @@ class MessageMetadata extends ChangeNotifier { notifyListeners(); } - MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); + MessageMetadata( + this.profileOnion, this.conversationIdentifier, this.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); } diff --git a/lib/models/servers.dart b/lib/models/servers.dart index bf9710dd..2850668b 100644 --- a/lib/models/servers.dart +++ b/lib/models/servers.dart @@ -24,12 +24,7 @@ class ServerListState extends ChangeNotifier { if (idx >= 0) { _servers[idx] = sis; } else { - _servers.add(ServerInfoState(onion: onion, - serverBundle: serverBundle, - running: running, - description: description, - autoStart: autoStart, - isEncrypted: isEncrypted)); + _servers.add(ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted)); } notifyListeners(); } @@ -37,7 +32,7 @@ class ServerListState extends ChangeNotifier { void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) { int idx = _servers.indexWhere((element) => element.onion == onion); 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 { print("Tried to update server list without a starting state...this is probably an error"); } diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 6b8f1e7c..2355fd57 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -296,11 +296,13 @@ class _AddEditProfileViewState extends State { // Profile Editing if (ctrlrPass.value.text.isEmpty) { // Don't update password, only update name + Provider.of(context, listen: false).nickname = ctrlrNick.value.text; Provider.of(context, listen: false).cwtch.SetProfileAttribute(Provider.of(context, listen: false).onion, "profile.name", ctrlrNick.value.text); Navigator.of(context).pop(); } else { // At this points passwords have been validated to be the same and not empty // Update both password and name, even if name hasn't been changed... + Provider.of(context, listen: false).nickname = ctrlrNick.value.text; Provider.of(context, listen: false).cwtch.SetProfileAttribute(Provider.of(context, listen: false).onion, "profile.name", ctrlrNick.value.text); final updatePasswordEvent = { "EventType": "ChangePassword", diff --git a/lib/views/addeditservers.dart b/lib/views/addeditservers.dart index c551ea4d..2a1c8f46 100644 --- a/lib/views/addeditservers.dart +++ b/lib/views/addeditservers.dart @@ -53,7 +53,6 @@ class _AddEditServerViewState extends State { @override Widget build(BuildContext context) { - return Scaffold( appBar: AppBar( title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle), @@ -82,232 +81,222 @@ class _AddEditServerViewState extends State { child: Form( key: _formKey, child: Container( - margin: EdgeInsets.fromLTRB(30, 0, 30, 10), - padding: EdgeInsets.fromLTRB(20, 0 , 20, 10), - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + margin: EdgeInsets.fromLTRB(30, 0, 30, 10), + padding: EdgeInsets.fromLTRB(20, 0, 20, 10), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, 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 - 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 - ) - ])), + // Description + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel), + Text(AppLocalizations.of(context)!.serverDescriptionDescription), + SizedBox( + height: 20, + ), + CwtchTextField( + controller: ctrlrDesc, + labelText: "Description", + autofocus: false, + ) + ]), - // Description - Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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( + height: 20, + ), - SizedBox( - height: 20, - ), + // Enabled + 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(context, listen: false).cwtch.LaunchServer(serverInfoState.onion); + } else { + Provider.of(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 - 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(context, listen: false).cwtch.LaunchServer(serverInfoState.onion); - } else { - Provider.of(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 + SwitchListTile( + title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())), + subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription), + value: serverInfoState.autoStart, + onChanged: (bool value) { + serverInfoState.setAutostart(value); - // Auto start - SwitchListTile( - title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())), - subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription), - value: serverInfoState.autoStart, - onChanged: (bool value) { - serverInfoState.setAutostart(value); + if (!serverInfoState.onion.isEmpty) { + Provider.of(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()), + ), - if (! serverInfoState.onion.isEmpty) { - Provider.of(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()), - ), + // ***** Password ***** + // use password toggle + Visibility( + visible: serverInfoState.onion.isEmpty, + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + 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: [ + 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(context).deletedServerError == true) { + return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer; + } + return null; + }, + ), + SizedBox( + height: 20, + ), + ])), - // use password toggle - Visibility( - visible: serverInfoState.onion.isEmpty, - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - 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, - ), - ])), + // new passwords 1 & 2 + 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, + ), - // current password - Visibility( - visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted, - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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(context).deletedServerError == true) { - return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer; - } - 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), + )) + ])) - // new passwords 1 & 2 - 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 ***** - - ])))))); + // ***** END Password ***** + ])))))); }); }); } @@ -318,29 +307,20 @@ class _AddEditServerViewState extends State { // match (and are provided if the user has requested an encrypted profile). if (_formKey.currentState!.validate()) { if (usePassword) { - Provider - .of(context, listen: false) - .cwtch - .CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of(context, listen: false).autoStart); + Provider.of(context, listen: false).cwtch.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of(context, listen: false).autoStart); } else { - Provider - .of(context, listen: false) - .cwtch - .CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of(context, listen: false).autoStart); + Provider.of(context, listen: false).cwtch.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of(context, listen: false).autoStart); } Navigator.of(context).pop(); } } void _savePressed() { - var server = Provider.of(context, listen: false); - Provider.of(context, listen: false) - .cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text); + Provider.of(context, listen: false).cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text); server.setDescription(ctrlrDesc.text); - if (_formKey.currentState!.validate()) { // TODO support change password } @@ -358,16 +338,11 @@ class _AddEditServerViewState extends State { Widget continueButton = ElevatedButton( child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn), onPressed: () { - var onion = Provider - .of(context, listen: false) - .onion; - Provider - .of(context, listen: false) - .cwtch - .DeleteServer(onion, Provider.of(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword); + var onion = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.DeleteServer(onion, Provider.of(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword); Future.delayed( const Duration(milliseconds: 500), - () { + () { if (globalErrorHandler.deletedServerSuccess) { final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion)); ScaffoldMessenger.of(context).showSnackBar(snackBar); @@ -395,4 +370,4 @@ class _AddEditServerViewState extends State { }, ); } -} \ No newline at end of file +} diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index bb9c585c..990899f3 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -112,7 +112,6 @@ class _ContactsViewState extends State { Clipboard.setData(new ClipboardData(text: Provider.of(context, listen: false).onion)); })); - // TODO servers // Search contacts diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index d8cb7b04..6668ed52 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -191,9 +191,8 @@ class _GlobalSettingsViewState extends State { secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()), ), Visibility( - visible: !Platform.isAndroid && !Platform.isIOS, - child: - SwitchListTile( + visible: !Platform.isAndroid && !Platform.isIOS, + child: SwitchListTile( title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor())), subtitle: Text(AppLocalizations.of(context)!.settingServersDescription), value: settings.isExperimentEnabled(ServerManagementExperiment), diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 79ae41e3..c2170a20 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -41,10 +41,10 @@ class _MessageViewState extends State { void initState() { scrollListener.itemPositions.addListener(() { if (scrollListener.itemPositions.value.length != 0 && - Provider.of(context, listen: false).unreadMessagesBelow == true && - scrollListener.itemPositions.value.any((element) => element.index == 0)) { - Provider.of(context, listen: false).initialScrollIndex = 0; - Provider.of(context, listen: false).unreadMessagesBelow = false; + Provider.of(context, listen: false).unreadMessagesBelow == true && + scrollListener.itemPositions.value.any((element) => element.index == 0)) { + Provider.of(context, listen: false).initialScrollIndex = 0; + Provider.of(context, listen: false).unreadMessagesBelow = false; } }); super.initState(); diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index de7f65b5..e4a6eba0 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -57,9 +57,9 @@ class _ProfileMgrViewState extends State { SizedBox( width: 10, ), - Expanded(child: Text(MediaQuery.of(context).size.width > 600 ? - AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort, - style: TextStyle(color: settings.current().mainTextColor()))) + Expanded( + child: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort, + style: TextStyle(color: settings.current().mainTextColor()))) ]), actions: getActions(), ), @@ -238,7 +238,7 @@ class _ProfileMgrViewState extends State { if (tiles.isEmpty) { return Center( child: Text( - AppLocalizations.of(context)!.unlockProfileTip, + AppLocalizations.of(context)!.unlockProfileTip, textAlign: TextAlign.center, )); } diff --git a/lib/views/serversview.dart b/lib/views/serversview.dart index 84c7e532..148d1daf 100644 --- a/lib/views/serversview.dart +++ b/lib/views/serversview.dart @@ -30,44 +30,45 @@ class _ServersView extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), - actions: getActions(), - ), - floatingActionButton: FloatingActionButton( - onPressed: _pushAddServer, - tooltip: AppLocalizations.of(context)!.addServerTooltip, - child: Icon( - Icons.add, - semanticLabel: AppLocalizations.of(context)!.addServerTooltip, + appBar: AppBar( + title: Text(MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), + actions: getActions(), ), - ), - body: Consumer( - builder: (context, svrs, child) { - final tiles = svrs.servers.map((ServerInfoState server) { - return ChangeNotifierProvider.value( - value: server, - builder: (context, child) => RepaintBoundary(child: ServerRow()), + floatingActionButton: FloatingActionButton( + onPressed: _pushAddServer, + tooltip: AppLocalizations.of(context)!.addServerTooltip, + child: Icon( + Icons.add, + semanticLabel: AppLocalizations.of(context)!.addServerTooltip, + ), + ), + body: Consumer( + builder: (context, svrs, child) { + final tiles = svrs.servers.map( + (ServerInfoState server) { + return ChangeNotifierProvider.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 getActions() { @@ -93,41 +94,41 @@ class _ServersView extends State { padding: MediaQuery.of(context).viewInsets, child: RepaintBoundary( child: Container( - height: 200, // bespoke value courtesy of the [TextField] docs - child: Center( - child: Padding( - padding: EdgeInsets.all(10.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text(AppLocalizations.of(context)!.enterServerPassword), - SizedBox( - height: 20, - ), - CwtchPasswordField( - autofocus: true, - controller: ctrlrPassword, - action: unlock, - validator: (value) {}, - ), - SizedBox( - height: 20, - ), - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Spacer(), - Expanded( - child: ElevatedButton( - child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock), - onPressed: () { - unlock(ctrlrPassword.value.text); - }, - )), - Spacer() - ]), - ], - ))), - ))); + height: 200, // bespoke value courtesy of the [TextField] docs + child: Center( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context)!.enterServerPassword), + SizedBox( + height: 20, + ), + CwtchPasswordField( + autofocus: true, + controller: ctrlrPassword, + action: unlock, + validator: (value) {}, + ), + SizedBox( + height: 20, + ), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + Spacer(), + Expanded( + child: ElevatedButton( + child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock), + onPressed: () { + unlock(ctrlrPassword.value.text); + }, + )), + Spacer() + ]), + ], + ))), + ))); }); } @@ -141,9 +142,11 @@ class _ServersView extends State { Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return MultiProvider( - providers: [ChangeNotifierProvider( - create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true), - )], + providers: [ + ChangeNotifierProvider( + create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true), + ) + ], child: AddEditServerView(), ); }, diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index e3aea951..bc75a6d9 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -89,15 +89,15 @@ class FileBubbleState extends State { } else if (flagStarted) { // in this case, the download was done in a previous application launch, // so we probably have to request an info lookup - if (!Provider.of(context).downloadInterrupted(widget.fileKey()) ) { + if (!Provider.of(context).downloadInterrupted(widget.fileKey())) { wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F'); Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, widget.fileKey()); } else { var path = Provider.of(context).downloadFinalPath(widget.fileKey()) ?? ""; - wdgDecorations = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children:[Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))] - ); + wdgDecorations = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'), + ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton)) + ]); } } else { wdgDecorations = Center( @@ -156,11 +156,7 @@ class FileBubbleState extends State { Provider.of(context, listen: false).flags |= 0x02; ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { - Provider - .of(context, listen: false) - .cwtch - .CreateDownloadableFile( - profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey()); + Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey()); } } else { try { @@ -176,11 +172,7 @@ class FileBubbleState extends State { Provider.of(context, listen: false).flags |= 0x02; ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { - Provider - .of(context, listen: false) - .cwtch - .DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, - widget.fileKey()); + Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey()); } } } catch (e) { diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 55e67bc2..ae68962b 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -52,7 +52,7 @@ class MessageBubbleState extends State { var wdgMessage; - if (!showClickableLinks) { + if (!showClickableLinks) { wdgMessage = SelectableText( widget.content + '\u202F', //key: Key(myKey), @@ -134,8 +134,7 @@ class MessageBubbleState extends State { mainAxisSize: MainAxisSize.min, children: [ 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?" - ), + "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: [ Container( margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10), @@ -170,6 +169,6 @@ class MessageBubbleState extends State { ], )), )); - }); + }); } } diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 0e9420e0..6c718a26 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -199,7 +199,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi ))))); var mark = Provider.of(context).newMarker; if (mark > 0 && mark == Provider.of(context).totalMessages - Provider.of(context).messageIndex) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Align(alignment:Alignment.center ,child:_bubbleNew()), mr]); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); } else { return mr; } @@ -209,9 +209,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi return Container( decoration: BoxDecoration( color: Provider.of(context).theme.messageFromMeBackgroundColor(), - border: Border.all( - color: Provider.of(context).theme.messageFromMeBackgroundColor(), - width: 1), + border: Border.all(color: Provider.of(context).theme.messageFromMeBackgroundColor(), width: 1), borderRadius: BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), @@ -219,9 +217,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi bottomRight: Radius.circular(8), ), ), - child: Padding( - padding: EdgeInsets.all(9.0), - child: Text(AppLocalizations.of(context)!.newMessagesLabel))); + child: Padding(padding: EdgeInsets.all(9.0), child: Text(AppLocalizations.of(context)!.newMessagesLabel))); } void _runAnimation(Offset pixelsPerSecond, Size size) { diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart index 574d7c7e..9c12f477 100644 --- a/lib/widgets/serverrow.dart +++ b/lib/widgets/serverrow.dart @@ -21,40 +21,38 @@ class _ServerRowState extends State { @override Widget build(BuildContext context) { var server = Provider.of(context); - return Card(clipBehavior: Clip.antiAlias, + return Card( + clipBehavior: Clip.antiAlias, margin: EdgeInsets.all(0.0), child: InkWell( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.all(6.0), //border size child: Icon(CwtchIcons.dns_24px, - color: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), - size: 64) - - ), + color: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), size: 64)), Expanded( child: Column( - children: [ - Text( - server.description, - semanticsLabel: server.description, - style: Provider.of(context).biggerFont.apply(color: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + children: [ + Text( + server.description, + semanticsLabel: server.description, + style: Provider.of(context) + .biggerFont + .apply(color: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + Visibility( + visible: !Provider.of(context).streamerMode, + child: ExcludeSemantics( + child: Text( + server.onion, softWrap: true, overflow: TextOverflow.ellipsis, - ), - Visibility( - visible: !Provider.of(context).streamerMode, - child: ExcludeSemantics( - child: Text( - server.onion, - softWrap: true, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), - ))) - ], - )), + style: TextStyle(color: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + ))) + ], + )), // Copy server button IconButton( @@ -75,23 +73,23 @@ class _ServerRowState extends State { _pushEditServer(server); }, ) - - ]))); } - void _pushEditServer(ServerInfoState server ) { + void _pushEditServer(ServerInfoState server) { Provider.of(context).reset(); Navigator.of(context).push(MaterialPageRoute( settings: RouteSettings(name: "serveraddedit"), builder: (BuildContext context) { return MultiProvider( - providers: [ChangeNotifierProvider( - create: (_) => server, - )], + providers: [ + ChangeNotifierProvider( + create: (_) => server, + ) + ], child: AddEditServerView(), ); }, )); } -} \ No newline at end of file +} From c00bfbb48b5363f5479c98acf391d50226289dd6 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 26 Nov 2021 14:25:21 -0800 Subject: [PATCH 11/28] Quoted Message Fixes / New Message Label Offet Fixes --- lib/cwtch/cwtch.dart | 4 ++++ lib/cwtch/ffi.dart | 13 +++++++++++++ lib/cwtch/gomobile.dart | 6 +++++- lib/models/message.dart | 16 ++++++++++++---- lib/views/messageview.dart | 5 +++-- lib/widgets/messagerow.dart | 8 ++++---- 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index cf42d0e0..d04fd13b 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -35,6 +35,10 @@ abstract class Cwtch { // ignore: non_constant_identifier_names Future GetMessage(String profile, int handle, int index); + + // ignore: non_constant_identifier_names + Future GetMessageByID(String profile, int handle, int index); + // ignore: non_constant_identifier_names Future GetMessageByContentHash(String profile, int handle, String contentHash); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 78be3e70..c59928e5 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -694,4 +694,17 @@ class CwtchFfi implements Cwtch { final Free = free.asFunction(); Free(ptr); } + + @override + Future GetMessageByID(String profile, int handle, int index) async { + var getMessageC = library.lookup>("c_GetMessageByID"); + // ignore: non_constant_identifier_names + final GetMessage = getMessageC.asFunction(); + final utf8profile = profile.toNativeUtf8(); + Pointer jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, handle, index); + String jsonMessage = jsonMessageBytes.toDartString(); + _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes); + malloc.free(utf8profile); + return jsonMessage; + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index f8d62f3a..fa39a0be 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -88,10 +88,14 @@ class CwtchGomobile implements Cwtch { // ignore: non_constant_identifier_names Future GetMessage(String profile, int handle, int index) { - print("gomobile.dart GetMessage " + index.toString()); return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index}); } + // ignore: non_constant_identifier_names + Future GetMessageByID(String profile, int handle, int index) { + return cwtchPlatform.invokeMethod("GetMessageByID", {"profile": profile, "contact": handle, "index": index}); + } + @override // ignore: non_constant_identifier_names void SendProfileEvent(String onion, String jsonEvent) { diff --git a/lib/models/message.dart b/lib/models/message.dart index 708cd8af..d6bac00f 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cwtch/config.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; @@ -31,9 +32,16 @@ abstract class Message { Widget getPreviewWidget(BuildContext context); } -Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index) { +Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) { try { - var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index); + Future rawMessageEnvelopeFuture; + + if (byID) { + rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, index); + } else { + rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index); + } + return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", null, 0, false, true); try { @@ -50,7 +58,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return messageHandler(context, profileOnion, conversationIdentifier, index).then((value) => value); + return messageHandler(context, profileOnion, conversationIdentifier, index, byID: byID).then((value) => value); }); } @@ -84,7 +92,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co return MalformedMessage(metadata); } } catch (e) { - print("an error! " + e.toString()); + EnvironmentConfig.debugLog("an error! " + e.toString()); return MalformedMessage(metadata); } }); diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index c2170a20..8c34116b 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -168,7 +168,7 @@ class _MessageViewState extends State { if (Provider.of(context, listen: false).selectedConversation != null && Provider.of(context, listen: false).selectedIndex != null) { Provider.of(context, listen: false) .cwtch - .GetMessage(Provider.of(context, listen: false).selectedProfile!, Provider.of(context, listen: false).selectedConversation!, + .GetMessageByID(Provider.of(context, listen: false).selectedProfile!, Provider.of(context, listen: false).selectedConversation!, Provider.of(context, listen: false).selectedIndex!) .then((data) { try { @@ -268,7 +268,8 @@ class _MessageViewState extends State { var children; if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { var quoted = FutureBuilder( - future: messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!), + future: + messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!, byID: true), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data! as Message; diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 6c718a26..feaeae15 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -77,7 +77,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi child: IconButton( tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage, onPressed: () { - Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageIndex; + Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageID; }, icon: Icon(Icons.reply, color: Provider.of(context).theme.dropShadowColor()))); Widget wdgSpacer = Flexible(child: SizedBox(width: 60, height: 10)); @@ -185,7 +185,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi }, onPanEnd: (details) { _runAnimation(details.velocity.pixelsPerSecond, size); - Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageIndex; + Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageID; }, child: Padding( padding: EdgeInsets.all(2), @@ -198,8 +198,8 @@ class MessageRowState extends State with SingleTickerProviderStateMi children: widgetRow, ))))); var mark = Provider.of(context).newMarker; - if (mark > 0 && mark == Provider.of(context).totalMessages - Provider.of(context).messageIndex) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); + if (mark > 0 && mark == Provider.of(context).messageIndex + 1) { + return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); } else { return mr; } From b0f74ffb6d3f3d8af1d7c5c5e30eafa4d7beaa4b Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 26 Nov 2021 15:07:37 -0800 Subject: [PATCH 12/28] Fixup Conversation Attribute Flow --- lib/cwtch/ffi.dart | 4 ++-- lib/views/groupsettingsview.dart | 2 +- lib/views/peersettingsview.dart | 9 ++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index c59928e5..23835614 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -537,12 +537,12 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names void SetConversationAttribute(String profile, int contact, String key, String val) { - var setContactAttribute = library.lookup>("c_SetContactAttribute"); + var setContactAttribute = library.lookup>("c_SetConversationAttribute"); // ignore: non_constant_identifier_names final SetContactAttribute = setContactAttribute.asFunction(); final u1 = profile.toNativeUtf8(); final u3 = key.toNativeUtf8(); - final u4 = key.toNativeUtf8(); + final u4 = val.toNativeUtf8(); SetContactAttribute(u1, u1.length, contact, u3, u3.length, u4, u4.length); malloc.free(u1); malloc.free(u3); diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 83e57d32..45dd0405 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -80,7 +80,7 @@ class _GroupSettingsViewState extends State { var profileOnion = Provider.of(context, listen: false).profileOnion; var handle = Provider.of(context, listen: false).identifier; Provider.of(context, listen: false).nickname = ctrlrNick.text; - Provider.of(context, listen: false).cwtch.SetConversationAttribute(profileOnion, handle, "local.name", ctrlrNick.text); + Provider.of(context, listen: false).cwtch.SetConversationAttribute(profileOnion, handle, "profile.name", ctrlrNick.text); // todo translations final snackBar = SnackBar(content: Text("Group Nickname changed successfully")); ScaffoldMessenger.of(context).showSnackBar(snackBar); diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 235ca8ca..506c1706 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -69,14 +69,9 @@ class _PeerSettingsViewState extends State { readonly: false, onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var onion = Provider.of(context, listen: false).onion; + var conversation = Provider.of(context, listen: false).identifier; Provider.of(context, listen: false).nickname = ctrlrNick.text; - final setPeerAttribute = { - "EventType": "SetPeerAttribute", - "Data": {"RemotePeer": onion, "Key": "local.name", "Data": ctrlrNick.text}, - }; - final setPeerAttributeJson = jsonEncode(setPeerAttribute); - Provider.of(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson); + Provider.of(context, listen: false).cwtch.SetConversationAttribute(profileOnion, conversation, "profile.name", ctrlrNick.text); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess)); ScaffoldMessenger.of(context).showSnackBar(snackBar); }, From d6839c62e38b5a65bb14c9567b99efedf92fa24f Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 1 Dec 2021 04:17:48 -0800 Subject: [PATCH 13/28] Move Attribute Updates / File Downloading / Contact Requests / Invites to new API --- lib/cwtch/cwtch.dart | 5 ++- lib/cwtch/cwtchNotifier.dart | 66 +++++++++++++++---------------- lib/cwtch/ffi.dart | 18 +++++++++ lib/cwtch/gomobile.dart | 5 +++ lib/models/message.dart | 20 ++++------ lib/views/groupsettingsview.dart | 5 ++- lib/views/messageview.dart | 4 +- lib/widgets/contactrow.dart | 2 + lib/widgets/filebubble.dart | 18 ++++----- lib/widgets/invitationbubble.dart | 10 ++--- 10 files changed, 87 insertions(+), 66 deletions(-) diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index d04fd13b..9e82c817 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -73,8 +73,9 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void SetProfileAttribute(String profile, String key, String val); // ignore: non_constant_identifier_names - void SetConversationAttribute(String profile, int 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 void LoadServers(String password); // ignore: non_constant_identifier_names diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 274026bf..6adb09b9 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -79,7 +79,6 @@ class CwtchNotifier { } break; case "GroupCreated": - // Retrieve Server Status from Cache... String status = ""; RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]); @@ -131,19 +130,24 @@ class CwtchNotifier { case "NewMessageFromPeer": notificationManager.notify("New Message From Peer!"); var identifier = int.parse(data["ConversationID"]); - if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++; - } else { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; - } - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; - profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); - // 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"; + // We might not have received a contact created for this contact yet... + // In that case the **next** event we receive will actually update these values... + if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier) != null) { + if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++; + } else { + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; + } + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; + profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); + + // 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; @@ -206,17 +210,7 @@ class CwtchNotifier { notificationManager.notify("New Message From Group!"); } } else { - // from me (already displayed - do not update counter) - var idx = data["Signature"]; - var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)?.getMessageKey(idx); - if (key == null) break; - try { - var message = Provider.of(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 - } + // This is not dealt with by IndexedAcknowledgment } break; case "MessageCounterResync": @@ -242,7 +236,7 @@ class CwtchNotifier { break; case "SendMessageToGroupError": // from me (already displayed - do not update counter) - EnvironmentConfig.debugLog("SendMessageToGroupError"); + EnvironmentConfig.debugLog("SendMessageToGroupError: $data"); var idx = data["Signature"]; var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["GroupID"])!.getMessageKey(idx); if (key == null) break; @@ -289,7 +283,6 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]); break; case "NewGroup": - EnvironmentConfig.debugLog("new group"); String invite = data["GroupInvite"].toString(); if (invite.startsWith("torv3")) { String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5))); @@ -303,7 +296,8 @@ class CwtchNotifier { } if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(groupInvite["GroupID"]) == null) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["ConversationID"], groupInvite["GroupID"], + var identifier = int.parse(data["ConversationID"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"], authorization: ContactAuthorization.approved, imagePath: data["PicturePath"], nickname: groupInvite["GroupName"], @@ -311,7 +305,7 @@ class CwtchNotifier { status: status, isGroup: true, 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; @@ -323,7 +317,7 @@ class CwtchNotifier { break; case "ServerStateChange": // Update the Server Cache - EnvironmentConfig.debugLog("server state changes $data"); + //EnvironmentConfig.debugLog("server state changes $data"); profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) { if (contact.isGroup == true && contact.server == data["GroupServer"]) { @@ -361,13 +355,17 @@ class CwtchNotifier { } break; case "NewRetValMessageFromPeer": - if (data["Path"] == "name" && data["Data"].toString().trim().length > 0) { - // Update locally on the UI... - if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.nickname = data["Data"]; + if (data["Path"] == "profile.name") { + if (data["Data"].toString().trim().length > 0) { + // Update locally on the UI... + 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 { - EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}"); + EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}"); } break; case "ManifestSaved": diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 23835614..38448abb 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -84,6 +84,9 @@ typedef VoidFromStringIntStringFn = void Function(Pointer, int, int, Point typedef void_from_string_int_string_string_function = Void Function(Pointer, Int32, Int32, Pointer, Int32, Pointer, Int32); typedef VoidFromStringIntStringStringFn = void Function(Pointer, int, int, Pointer, int, Pointer, int); +typedef void_from_string_int_int_int_string_string_function = Void Function(Pointer, Int32, Int32, Int32, Int32, Pointer, Int32, Pointer, Int32); +typedef VoidFromStringIntIntIntStringStringFn = void Function(Pointer, int, int, int, int, Pointer, int, Pointer, int); + typedef void_from_string_int_int_function = Void Function(Pointer, Int32, Int32, Int32); typedef VoidFromStringIntIntFn = void Function(Pointer, int, int, int); @@ -549,6 +552,21 @@ class CwtchFfi implements Cwtch { 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>("c_SetMessageAttribute"); + // ignore: non_constant_identifier_names + final SetMessageAttribute = setMessageAttribute.asFunction(); + 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(u3); + malloc.free(u4); + } + @override // ignore: non_constant_identifier_names void LoadServers(String password) { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index fa39a0be..a2cac25d 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -281,4 +281,9 @@ class CwtchGomobile implements Cwtch { Future GetMessageByContentHash(String profile, int handle, String contentHash) { return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "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}); + } } diff --git a/lib/models/message.dart b/lib/models/message.dart index d6bac00f..9789bba9 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -43,7 +43,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co } return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", null, 0, false, true); + var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", {}, false, true); try { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); // There are 2 conditions in which this error condition can be met: @@ -67,11 +67,11 @@ Future messageHandler(BuildContext context, String profileOnion, int co var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; var senderHandle = messageWrapper['PeerID']; var senderImage = messageWrapper['ContactImage']; - var flags = int.parse(messageWrapper['Flags'].toString()); + var attributes = messageWrapper['Attributes']; var ackd = messageWrapper['Acknowledged']; var error = messageWrapper['Error'] != null; var signature = messageWrapper['Signature']; - metadata = MessageMetadata(profileOnion, conversationIdentifier, index, messageID, timestamp, senderHandle, senderImage, signature, flags, ackd, error); + metadata = MessageMetadata(profileOnion, conversationIdentifier, index, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error); dynamic message = jsonDecode(messageWrapper['Message']); var content = message['d'] as dynamic; @@ -97,7 +97,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co } }); } catch (e) { - return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", null, 0, false, true))); + return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", {}, false, true))); } } @@ -111,17 +111,13 @@ class MessageMetadata extends ChangeNotifier { final DateTime timestamp; final String senderHandle; final String? senderImage; - int _flags; + final dynamic _attributes; bool _ackd; bool _error; final String? signature; - int get flags => this._flags; - set flags(int newVal) { - this._flags = newVal; - notifyListeners(); - } + dynamic get attributes => this._attributes; bool get ackd => this._ackd; set ackd(bool newVal) { @@ -135,6 +131,6 @@ class MessageMetadata extends ChangeNotifier { notifyListeners(); } - MessageMetadata( - this.profileOnion, this.conversationIdentifier, this.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._flags, this._ackd, this._error); + MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, + this._error); } diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 45dd0405..463a99b2 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -195,10 +195,11 @@ class _GroupSettingsViewState extends State { child: Text(AppLocalizations.of(context)!.yesLeave), onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).identifier; + var identifier = Provider.of(context, listen: false).identifier; // locally update cache... Provider.of(context, listen: false).isArchived = true; - Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, handle); + Provider.of(context, listen: false).contactList.removeContact(identifier); + Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, identifier); Future.delayed(Duration(milliseconds: 500), () { Provider.of(context, listen: false).selectedConversation = null; Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 8c34116b..2b5d225d 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -353,7 +353,7 @@ class _MessageViewState extends State { return contact.onion != Provider.of(context).onion; }, onChanged: (newVal) { setState(() { - this.selectedContact = newVal; + this.selectedContact = Provider.of(context).contactList.findContact(newVal)!.identifier; }); })), SizedBox( @@ -362,7 +362,7 @@ class _MessageViewState extends State { ElevatedButton( child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn), onPressed: () { - if (this.selectedContact != "") { + if (this.selectedContact != -1) { this._sendInvitation(); } Navigator.pop(bcontext); diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index c34e4668..d013e28b 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -111,6 +111,8 @@ class _ContactRowState extends State { } void _btnApprove() { + // Update the UI + Provider.of(context, listen: false).authorization = ContactAuthorization.approved; Provider.of(context, listen: false) .cwtch .AcceptContact(Provider.of(context, listen: false).profileOnion, Provider.of(context, listen: false).identifier); diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index bc75a6d9..ea26052a 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:cwtch/config.dart'; import 'package:cwtch/models/message.dart'; import 'package:file_picker_desktop/file_picker_desktop.dart'; import 'package:flutter/cupertino.dart'; @@ -41,7 +42,7 @@ class FileBubbleState extends State { @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; - var flagStarted = Provider.of(context).flags & 0x02 > 0; + var flagStarted = Provider.of(context).attributes["file-downloaded"] == "true"; var borderRadiousEh = 15.0; var showFileSharing = Provider.of(context).isExperimentEnabled(FileSharingExperiment); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); @@ -146,14 +147,13 @@ class FileBubbleState extends State { String? selectedFileName; File? file; var profileOnion = Provider.of(context, listen: false).onion; - var handle = Provider.of(context, listen: false).senderHandle; - var contact = Provider.of(context, listen: false).onion; - var idx = Provider.of(context, listen: false).messageIndex; + var conversation = Provider.of(context, listen: false).identifier; + var idx = Provider.of(context, listen: false).messageID; if (Platform.isAndroid) { Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); - //Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); - Provider.of(context, listen: false).flags |= 0x02; + Provider.of(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true"); + //Provider.of(context, listen: false).attributes |= 0x02; ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey()); @@ -165,11 +165,11 @@ class FileBubbleState extends State { ); if (selectedFileName != null) { file = File(selectedFileName); - print("saving to " + file.path); + EnvironmentConfig.debugLog("saving to " + file.path); var manifestPath = file.path + ".manifest"; Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); - //Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); - Provider.of(context, listen: false).flags |= 0x02; + Provider.of(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true"); + //Provider.of(context, listen: false).flags |= 0x02; ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey()); diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index e3452d91..483ed833 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -39,7 +39,7 @@ class InvitationBubbleState extends State { isAccepted = Provider.of(context).contactList.findContact(widget.inviteTarget) != null; var borderRadiousEh = 15.0; var showGroupInvite = Provider.of(context).isExperimentEnabled(TapirGroupsExperiment); - rejected = Provider.of(context).flags & 0x01 == 0x01; + rejected = Provider.of(context).attributes["rejected-invite"] == "true"; var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); // If the sender is not us, then we want to give them a nickname... @@ -128,10 +128,10 @@ class InvitationBubbleState extends State { void _btnReject() { setState(() { var profileOnion = Provider.of(context, listen: false).onion; - var contact = Provider.of(context, listen: false).onion; - var idx = Provider.of(context, listen: false).messageIndex; - //Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); - Provider.of(context, listen: false).flags |= 0x01; + var conversation = Provider.of(context, listen: false).identifier; + var idx = Provider.of(context, listen: false).messageID; + Provider.of(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "rejected-invite", "true"); + //Provider.of(context, listen: false).flags |= 0x01; }); } From c42be6224d33465760119ce334ffc9a2094f5bb6 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 3 Dec 2021 11:28:10 -0800 Subject: [PATCH 14/28] Porting Android over to new API --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 105 +++++++++--------- .../kotlin/im/cwtch/flwtch/MainActivity.kt | 1 + lib/cwtch/cwtch.dart | 4 - lib/cwtch/cwtchNotifier.dart | 40 ++----- lib/cwtch/ffi.dart | 21 ---- lib/cwtch/gomobile.dart | 75 +++++-------- lib/model.dart | 15 ++- lib/models/messages/filemessage.dart | 5 +- lib/models/messages/invitemessage.dart | 5 +- lib/models/messages/quotedmessage.dart | 4 +- lib/models/messages/textmessage.dart | 9 +- lib/widgets/contactrow.dart | 2 +- lib/widgets/messagelist.dart | 2 +- lib/widgets/messageloadingbubble.dart | 5 - lib/widgets/messagerow.dart | 8 +- 15 files changed, 132 insertions(+), 169 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index c82d44ff..7adbdfa9 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -181,61 +181,69 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : Cwtch.loadProfiles(pass) } "GetMessage" -> { - val profile = (a.get("profile") as? String) ?: "" - val handle = (a.get("contact") as? String) ?: "" - val indexI = a.getInt("index") - return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build()) + val profile = (a.get("ProfileOnion") as? String) ?: "" + val conversation = a.getInt("conversation").toLong() + val indexI = a.getInt("index").toLong() + return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build()) + } + "GetMessageByID" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val conversation = a.getInt("conversation").toLong() + val id = a.getInt("id").toLong() + return Result.success(Data.Builder().putString("result", Cwtch.getMessageByID(profile, conversation, id)).build()) } "GetMessageByContentHash" -> { - val profile = (a.get("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 handle = (a.get("handle") as? String) ?: "" - Cwtch.acceptContact(profile, handle) + val conversation = a.getInt("conversation").toLong() + val contentHash = (a.get("contentHash") as? String) ?: "" + return Result.success(Data.Builder().putString("result", Cwtch.getMessagesByContentHash(profile, conversation, contentHash)).build()) + } + "UpdateMessageAttribute" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val conversation = a.getInt("conversation").toLong() + val channel = a.getInt("chanenl").toLong() + val midx = a.getInt("midx").toLong() + val key = (a.get("key") as? String) ?: "" + val value = (a.get("value") as? String) ?: "" + Cwtch.setMessageAttribute(profile, conversation, channel, midx, key, value) + } + "AcceptConversation" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val conversation = a.getInt("conversation").toLong() + Cwtch.acceptConversation(profile, conversation) } "BlockContact" -> { val profile = (a.get("ProfileOnion") as? String) ?: "" - val handle = (a.get("handle") as? String) ?: "" - Cwtch.blockContact(profile, handle) + val conversation = a.getInt("conversation").toLong() + Cwtch.blockContact(profile, conversation) } "SendMessage" -> { 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) ?: "" - Cwtch.sendMessage(profile, handle, message) + Cwtch.sendMessage(profile, conversation, message) } "SendInvitation" -> { val profile = (a.get("ProfileOnion") as? String) ?: "" - val handle = (a.get("handle") as? String) ?: "" - val target = (a.get("target") as? String) ?: "" - Cwtch.sendInvitation(profile, handle, target) + val conversation = a.getInt("conversation").toLong() + val target = (a.get("target") as? Long) ?: -1 + Cwtch.sendInvitation(profile, conversation, target) } "ShareFile" -> { 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) ?: "" - Cwtch.shareFile(profile, handle, filepath) + Cwtch.shareFile(profile, conversation, filepath) } "DownloadFile" -> { 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 manifestpath = (a.get("manifestpath") as? String) ?: "" val filekey = (a.get("filekey") as? String) ?: "" // FIXME: Prevent spurious calls by Intent if (profile != "") { - Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey) + Cwtch.downloadFile(profile, conversation, filepath, manifestpath, filekey) } } "CheckDownloadStatus" -> { @@ -245,9 +253,9 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : } "VerifyOrResumeDownload" -> { 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) ?: "" - Cwtch.verifyOrResumeDownload(profile, handle, fileKey) + Cwtch.verifyOrResumeDownload(profile, conversation, fileKey) } "SendProfileEvent" -> { val onion = (a.get("onion") as? String) ?: "" @@ -266,13 +274,6 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val bundle = (a.get("bundle") as? String) ?: "" Cwtch.importBundle(profile, bundle) } - "SetGroupAttribute" -> { - val profile = (a.get("ProfileOnion") as? String) ?: "" - val groupHandle = (a.get("groupHandle") as? String) ?: "" - val key = (a.get("key") as? String) ?: "" - val value = (a.get("value") as? String) ?: "" - Cwtch.setGroupAttribute(profile, groupHandle, key, value) - } "CreateGroup" -> { val profile = (a.get("ProfileOnion") as? String) ?: "" val server = (a.get("server") as? String) ?: "" @@ -286,18 +287,13 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : } "ArchiveConversation" -> { val profile = (a.get("ProfileOnion") as? String) ?: "" - val contactHandle = (a.get("handle") as? String) ?: "" - Cwtch.archiveConversation(profile, contactHandle) + val conversation = (a.get("conversation") as? Long) ?: -1 + Cwtch.archiveConversation(profile, conversation) } - "DeleteContact" -> { + "DeleteConversation" -> { val profile = (a.get("ProfileOnion") as? String) ?: "" - val handle = (a.get("handle") as? String) ?: "" - Cwtch.deleteContact(profile, handle) - } - "RejectInvite" -> { - val profile = (a.get("ProfileOnion") as? String) ?: "" - val groupHandle = (a.get("groupHandle") as? String) ?: "" - Cwtch.rejectInvite(profile, groupHandle) + val conversation = (a.get("conversation") as? Long) ?: -1 + Cwtch.deleteContact(profile, conversation) } "SetProfileAttribute" -> { val profile = (a.get("ProfileOnion") as? String) ?: "" @@ -305,12 +301,12 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val v = (a.get("Val") as? String) ?: "" Cwtch.setProfileAttribute(profile, key, v) } - "SetContactAttribute" -> { + "SetConversationAttribute" -> { 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 v = (a.get("Val") as? String) ?: "" - Cwtch.setContactAttribute(profile, contact, key, v) + Cwtch.setConversationAttribute(profile, conversation, key, v) } "Shutdown" -> { Cwtch.shutdownCwtch(); @@ -354,7 +350,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val v = (a.get("Val") as? String) ?: "" Cwtch.setServerAttribute(serverOnion, key, v) } - else -> return Result.failure() + else -> { + Log.i("FlwtchWorker", "unknown command: " + method); + return Result.failure() + } } return Result.success() } diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index d0f6503d..61447ef5 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -152,6 +152,7 @@ class MainActivity: FlutterActivity() { Log.i("handleCwtch:WorkManager", "canceling ${workInfo.id} bc tags don't include $uniqueTag") WorkManager.getInstance(this).cancelWorkById(workInfo.id) } + WorkManager.getInstance(this).cancelWorkById(workInfo.id) } WorkManager.getInstance(this).pruneWork() diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 9e82c817..a2606b27 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -10,8 +10,6 @@ abstract class Cwtch { // ignore: non_constant_identifier_names Future ReconnectCwtchForeground(); - // ignore: non_constant_identifier_names - void SelectProfile(String onion); // ignore: non_constant_identifier_names void CreateProfile(String nick, String pass); // ignore: non_constant_identifier_names @@ -69,8 +67,6 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void ImportBundle(String profile, String bundle); // ignore: non_constant_identifier_names - void RejectInvite(String profileOnion, int groupHandle); - // ignore: non_constant_identifier_names void SetProfileAttribute(String profile, String key, String val); // ignore: non_constant_identifier_names void SetConversationAttribute(String profile, int conversation, String key, String val); diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 6adb09b9..c68a89d1 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -139,8 +139,8 @@ class CwtchNotifier { } else { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; // 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 @@ -155,13 +155,12 @@ class CwtchNotifier { // We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end... break; case "IndexedAcknowledgement": - var messageID = data["Index"]; - var identifier = int.parse(data["ConversationID"]); - var idx = identifier.toString() + messageID; - + var conversation = int.parse(data["ConversationID"]); + var message_index = 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 - if (idx == "-1") break; - var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.getMessageKey(idx); + if (message_index == -1) break; + var key = contact!.getMessageKeyOrFail(conversation, message_index, contact.lastMessageTime); if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); @@ -169,8 +168,8 @@ class CwtchNotifier { // 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 // end during syncing. - if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated"; + if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) { + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated"; } message.ackd = true; } catch (e) { @@ -225,27 +224,12 @@ class CwtchNotifier { // Ignore break; case "IndexedFailure": - var idx = data["Index"]; - var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])?.getMessageKey(idx); - try { - var message = Provider.of(key!.currentContext!, listen: false); - 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: $data"); - var idx = data["Signature"]; - var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["GroupID"])!.getMessageKey(idx); - if (key == null) break; - try { + var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]); + var idx = int.parse(data["Index"]); + var key = contact?.getMessageKeyOrFail(contact.identifier, idx, contact.lastMessageTime); + if (key != null) { var message = Provider.of(key.currentContext!, listen: false); - if (message == null) break; message.error = true; - } catch (e) { - // ignore, we likely have an old key that has been replaced with an actual signature } break; case "AppError": diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 38448abb..37ea80a8 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -261,16 +261,6 @@ class CwtchFfi implements Cwtch { } } - // ignore: non_constant_identifier_names - void SelectProfile(String onion) async { - var selectProfileC = library.lookup>("c_SelectProfile"); - // ignore: non_constant_identifier_names - final SelectProfile = selectProfileC.asFunction(); - final ut8Onion = onion.toNativeUtf8(); - SelectProfile(ut8Onion, ut8Onion.length); - malloc.free(ut8Onion); - } - // ignore: non_constant_identifier_names void CreateProfile(String nick, String pass) { var createProfileC = library.lookup>("c_CreateProfile"); @@ -460,17 +450,6 @@ class CwtchFfi implements Cwtch { malloc.free(u2); } - @override - // ignore: non_constant_identifier_names - void RejectInvite(String profileOnion, int groupHandle) { - var rejectInvite = library.lookup>("c_RejectInvite"); - // ignore: non_constant_identifier_names - final RejectInvite = rejectInvite.asFunction(); - final u1 = profileOnion.toNativeUtf8(); - RejectInvite(u1, u1.length, groupHandle); - malloc.free(u1); - } - @override // ignore: non_constant_identifier_names void CreateGroup(String profileOnion, String server, String groupName) { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index a2cac25d..1b4c7fd0 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -66,11 +66,6 @@ class CwtchGomobile implements Cwtch { cwtchNotifier.handleMessage(call.method, obj); } - // ignore: non_constant_identifier_names - void SelectProfile(String onion) { - cwtchPlatform.invokeMethod("SelectProfile", {"profile": onion}); - } - // ignore: non_constant_identifier_names void CreateProfile(String nick, String pass) { cwtchPlatform.invokeMethod("CreateProfile", {"nick": nick, "pass": pass}); @@ -87,13 +82,13 @@ class CwtchGomobile implements Cwtch { } // ignore: non_constant_identifier_names - Future GetMessage(String profile, int handle, int index) { - return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index}); + Future GetMessage(String profile, int conversation, int index) { + return cwtchPlatform.invokeMethod("GetMessage", {"ProfileOnion": profile, "conversation": conversation, "index": index}); } // ignore: non_constant_identifier_names - Future GetMessageByID(String profile, int handle, int index) { - return cwtchPlatform.invokeMethod("GetMessageByID", {"profile": profile, "contact": handle, "index": index}); + Future GetMessageByID(String profile, int conversation, int id) { + return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id}); } @override @@ -113,43 +108,43 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void AcceptContact(String profileOnion, int contactHandle) { - cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); + void AcceptContact(String profileOnion, int conversation) { + cwtchPlatform.invokeMethod("AcceptContact", {"ProfileOnion": profileOnion, "conversation": conversation}); } @override // ignore: non_constant_identifier_names - void BlockContact(String profileOnion, int contactHandle) { - cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); + void BlockContact(String profileOnion, int conversation) { + cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "conversation": conversation}); } @override // ignore: non_constant_identifier_names - void SendMessage(String profileOnion, int contactHandle, String message) { - cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "handle": contactHandle, "message": message}); + void SendMessage(String profileOnion, int conversation, String message) { + cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message}); } @override // ignore: non_constant_identifier_names - void SendInvitation(String profileOnion, int contactHandle, int target) { - cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target}); + void SendInvitation(String profileOnion, int conversation, int target) { + cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target}); } @override // ignore: non_constant_identifier_names - void ShareFile(String profileOnion, int contactHandle, String filepath) { - cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath}); + void ShareFile(String profileOnion, int conversation, String filepath) { + cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath}); } @override // ignore: non_constant_identifier_names - void DownloadFile(String profileOnion, int contactHandle, String filepath, String manifestpath, String filekey) { - cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey}); + void DownloadFile(String profileOnion, int conversation, String filepath, String manifestpath, String filekey) { + cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey}); } // ignore: non_constant_identifier_names - void CreateDownloadableFile(String profileOnion, int contactHandle, String filenameSuggestion, String filekey) { - cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey}); + void CreateDownloadableFile(String profileOnion, int conversation, String filenameSuggestion, String filekey) { + cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filename": filenameSuggestion, "filekey": filekey}); } @override @@ -160,8 +155,8 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void VerifyOrResumeDownload(String profileOnion, int contactHandle, String filekey) { - cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "handle": contactHandle, "filekey": filekey}); + void VerifyOrResumeDownload(String profileOnion, int conversation, String filekey) { + cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "conversation": conversation, "filekey": filekey}); } @override @@ -176,18 +171,6 @@ class CwtchGomobile implements Cwtch { 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, int groupHandle) { - cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle}); - } - @override void CreateGroup(String profileOnion, String server, String groupName) { cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName}); @@ -195,14 +178,14 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void DeleteContact(String profileOnion, int handle) { - cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "handle": handle}); + void DeleteContact(String profileOnion, int conversation) { + cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "conversation": conversation}); } @override // ignore: non_constant_identifier_names - void ArchiveConversation(String profileOnion, int contactHandle) { - cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "handle": contactHandle}); + void ArchiveConversation(String profileOnion, int conversation) { + cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "conversation": conversation}); } @override @@ -213,8 +196,8 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void SetConversationAttribute(String profile, int contact, String key, String val) { - cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "Contact": contact, "Key": key, "Val": val}); + void SetConversationAttribute(String profile, int conversation, String key, String val) { + cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "conversation": conversation, "Key": key, "Val": val}); } @override @@ -278,12 +261,12 @@ class CwtchGomobile implements Cwtch { } @override - Future GetMessageByContentHash(String profile, int handle, String contentHash) { - return cwtchPlatform.invokeMethod("GetMessageByContentHash", {"profile": profile, "contact": handle, "contentHash": contentHash}); + Future GetMessageByContentHash(String profile, int conversation, String 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}); + cwtchPlatform.invokeMethod("SetMessageAttribute", {"ProfileOnion": profile, "conversation": conversation, "Channel": channel, "Message": message, "Key": key, "Val": val}); } } diff --git a/lib/model.dart b/lib/model.dart index 1526d190..a0db9ead 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cwtch/config.dart'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/cupertino.dart'; import 'package:cwtch/models/profileservers.dart'; @@ -657,11 +658,23 @@ class ContactInfoState extends ChangeNotifier { } } - GlobalKey getMessageKey(String index) { + GlobalKey getMessageKey(int conversation, int message, DateTime lastread) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); //+ " lr:" +lastMessageTime.toIso8601String(); + //EnvironmentConfig.debugLog("looked up key $index"); if (keys[index] == null) { keys[index] = GlobalKey(); } GlobalKey ret = keys[index]!; return ret; } + + GlobalKey? getMessageKeyOrFail(int conversation, int message, DateTime lastread) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); // + " lr:" +lastMessageTime.toIso8601String(); + + if (keys[index] == null) { + return null; + } + GlobalKey ret = keys[index]!; + return ret; + } } diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index 24c6e152..3097516c 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -21,7 +21,6 @@ class FileMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); dynamic shareObj = jsonDecode(this.content); if (shareObj == null) { return MessageRow(MalformedBubble()); @@ -35,7 +34,9 @@ class FileMessage extends Message { return MessageRow(MalformedBubble()); } - return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of(bcontext).getMessageKey(idx)); + var lrt = Provider.of(bcontext).lastMessageTime; + return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), + key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); }); } diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index fb58f083..fda0e7fe 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -21,7 +21,6 @@ class InviteMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); String inviteTarget; String inviteNick; String invite = this.content; @@ -40,7 +39,9 @@ class InviteMessage extends Message { return MessageRow(MalformedBubble()); } } - return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: Provider.of(bcontext).getMessageKey(idx)); + var lrt = Provider.of(bcontext).lastMessageTime; + return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), + key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); }); } diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 6ddba3a3..fb835989 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -94,7 +94,7 @@ class QuotedMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); + var lrt = Provider.of(bcontext).lastMessageTime; return MessageRow( QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { if (localIndex != null) { @@ -102,7 +102,7 @@ class QuotedMessage extends Message { } return MalformedMessage(this.metadata); })), - key: Provider.of(bcontext).getMessageKey(idx)); + key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); }); } catch (e) { return MalformedMessage(this.metadata).getWidget(context); diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index ac996152..d1f7fd6b 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -1,5 +1,8 @@ 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/messageloadingbubble.dart'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -32,8 +35,10 @@ class TextMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - String idx = this.metadata.conversationIdentifier.toString() + this.metadata.messageID.toString(); - return MessageRow(MessageBubble(this.content), key: Provider.of(bcontext).getMessageKey(idx)); + var lrt = Provider.of(bcontext).lastMessageTime; + var key = Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt); + + return MessageRow(MessageBubble(this.content), key: key); }); } } diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index d013e28b..93bbb655 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -121,7 +121,7 @@ class _ContactRowState extends State { void _btnReject() { ContactInfoState contact = Provider.of(context, listen: false); if (contact.isGroup == true) { - Provider.of(context, listen: false).cwtch.RejectInvite(Provider.of(context, listen: false).profileOnion, contact.identifier); + // FIXME This flow is incrorect. Groups never just show up on the contact list anymore Provider.of(context, listen: false).removeContact(contact.onion); } else { Provider.of(context, listen: false).cwtch.BlockContact(Provider.of(context, listen: false).profileOnion, contact.identifier); diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 3954ea74..f47b03c9 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -87,7 +87,7 @@ class _MessageListState extends State { // Already includes MessageRow,, return message.getWidget(context); } else { - return Text(''); //MessageLoadingBubble(); + return MessageLoadingBubble(); } }, ); diff --git a/lib/widgets/messageloadingbubble.dart b/lib/widgets/messageloadingbubble.dart index f9605d15..aeba1657 100644 --- a/lib/widgets/messageloadingbubble.dart +++ b/lib/widgets/messageloadingbubble.dart @@ -1,9 +1,4 @@ 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 { @override diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index feaeae15..963a41ab 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -15,6 +15,7 @@ import '../settings.dart'; class MessageRow extends StatefulWidget { final Widget child; + MessageRow(this.child, {Key? key}) : super(key: key); @override @@ -28,9 +29,12 @@ class MessageRowState extends State with SingleTickerProviderStateMi late Alignment _dragAlignment = Alignment.center; Alignment _dragAffinity = Alignment.center; + late int index; + @override void initState() { super.initState(); + index = Provider.of(context, listen: false).messageIndex; _controller = AnimationController(vsync: this); _controller.addListener(() { setState(() { @@ -41,7 +45,9 @@ class MessageRowState extends State with SingleTickerProviderStateMi @override void dispose() { - _controller.dispose(); + if (_controller != null) { + _controller.dispose(); + } super.dispose(); } From c9319d32d0d88cf1ed9b49375b13c96db8d34a45 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Dec 2021 12:25:17 -0800 Subject: [PATCH 15/28] Much improved message caching --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 1 + .../kotlin/im/cwtch/flwtch/MainActivity.kt | 1 + lib/cwtch/cwtchNotifier.dart | 23 +++++-- lib/model.dart | 17 +++++ lib/models/message.dart | 65 +++++++++++-------- lib/models/messages/filemessage.dart | 7 +- lib/models/messages/invitemessage.dart | 6 +- lib/models/messages/malformedmessage.dart | 3 +- lib/models/messages/quotedmessage.dart | 29 ++++----- lib/models/messages/textmessage.dart | 7 +- lib/views/messageview.dart | 2 + lib/widgets/messagelist.dart | 26 +++++++- lib/widgets/messagerow.dart | 17 +++-- 13 files changed, 137 insertions(+), 67 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 7adbdfa9..1ab53ec9 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -184,6 +184,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val profile = (a.get("ProfileOnion") as? String) ?: "" val conversation = a.getInt("conversation").toLong() val indexI = a.getInt("index").toLong() + Log.i("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString()) return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build()) } "GetMessageByID" -> { diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index 61447ef5..dec5f220 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -30,6 +30,7 @@ import android.os.Build import android.os.Environment import android.database.Cursor import android.provider.MediaStore +import cwtch.Cwtch class MainActivity: FlutterActivity() { override fun provideSplashScreen(): SplashScreen? = SplashView() diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index c68a89d1..72d68f26 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -130,6 +130,10 @@ class CwtchNotifier { case "NewMessageFromPeer": notificationManager.notify("New Message From Peer!"); var identifier = int.parse(data["ConversationID"]); + var messageID = int.parse(data["Index"]); + var timestamp = DateTime.tryParse(data['TimestampReceived'])!; + var senderHandle = data['RemotePeer']; + var senderImage = data['Picture']; // We might not have received a contact created for this contact yet... // In that case the **next** event we receive will actually update these values... @@ -140,6 +144,7 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"], ""); profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; // We only ever see messages from authenticated peers. @@ -156,11 +161,12 @@ class CwtchNotifier { break; case "IndexedAcknowledgement": var conversation = int.parse(data["ConversationID"]); - var message_index = int.parse(data["Index"]); + 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 - if (message_index == -1) break; - var key = contact!.getMessageKeyOrFail(conversation, message_index, contact.lastMessageTime); + if (messageID == -1) break; + var key = contact!.getMessageKeyOrFail(conversation, messageID, contact.lastMessageTime); if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); @@ -181,11 +187,19 @@ class CwtchNotifier { var identifier = int.parse(data["ConversationID"]); if (data["ProfileOnion"] != data["RemotePeer"]) { var idx = int.parse(data["Index"]); + 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... if (currentTotal != null && idx >= currentTotal) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages = idx + 1; + profileCN + .getProfile(data["ProfileOnion"]) + ?.contactList + .getContact(identifier)! + .updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"], data["Signature"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; //if not currently open if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { @@ -194,7 +208,6 @@ class CwtchNotifier { 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. // 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 diff --git a/lib/model.dart b/lib/model.dart index a0db9ead..f08113cf 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:cwtch/config.dart'; +import 'package:cwtch/models/message.dart'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/cupertino.dart'; import 'package:cwtch/models/profileservers.dart'; @@ -501,6 +502,12 @@ ContactAuthorization stringToContactAuthorization(String authStr) { } } +class MessageCache { + final MessageMetadata metadata; + final String wrapper; + MessageCache(this.metadata, this.wrapper); +} + class ContactInfoState extends ChangeNotifier { final String profileOnion; final int identifier; @@ -515,6 +522,7 @@ class ContactInfoState extends ChangeNotifier { late int _totalMessages = 0; late DateTime _lastMessageTime; late Map> keys; + late List messageCache; int _newMarker = 0; DateTime _newMarkerClearAt = DateTime.now(); @@ -546,6 +554,7 @@ class ContactInfoState extends ChangeNotifier { this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._server = server; this._archived = archived; + this.messageCache = List.empty(growable: true); keys = Map>(); } @@ -677,4 +686,12 @@ class ContactInfoState extends ChangeNotifier { GlobalKey ret = keys[index]!; return ret; } + + void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data, String signature) { + this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, signature, {}, false, false), data)); + } + + void bumpMessageCache() { + this.messageCache.insert(0, null); + } } diff --git a/lib/models/message.dart b/lib/models/message.dart index 9789bba9..0fab66e9 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -28,11 +28,43 @@ const GroupConversationHandleLength = 32; abstract class Message { MessageMetadata getMetadata(); - Widget getWidget(BuildContext context); + Widget getWidget(BuildContext context, Key key); Widget getPreviewWidget(BuildContext context); } +Message compileOverlay(MessageMetadata metadata, String messageData) { + try { + 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 messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) { + var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)!.messageCache; + if (cache.length > index) { + if (cache[index] != null) { + return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper)); + } + } + try { Future rawMessageEnvelopeFuture; @@ -43,7 +75,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co } return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", {}, false, true); + var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", {}, false, true); try { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); // There are 2 conditions in which this error condition can be met: @@ -58,7 +90,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return messageHandler(context, profileOnion, conversationIdentifier, index, byID: byID).then((value) => value); + return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value); }); } @@ -71,33 +103,16 @@ Future messageHandler(BuildContext context, String profileOnion, int co var ackd = messageWrapper['Acknowledged']; var error = messageWrapper['Error'] != null; var signature = messageWrapper['Signature']; - metadata = MessageMetadata(profileOnion, conversationIdentifier, index, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error); + metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error); - dynamic message = jsonDecode(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); - } + return compileOverlay(metadata, messageWrapper['Message']); } catch (e) { EnvironmentConfig.debugLog("an error! " + e.toString()); return MalformedMessage(metadata); } }); } catch (e) { - return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, index, -1, DateTime.now(), "", "", "", {}, false, true))); + return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", {}, false, true))); } } @@ -105,7 +120,6 @@ class MessageMetadata extends ChangeNotifier { // meta-metadata final String profileOnion; final int conversationIdentifier; - final int messageIndex; final int messageID; final DateTime timestamp; @@ -131,6 +145,5 @@ class MessageMetadata extends ChangeNotifier { notifyListeners(); } - MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageIndex, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, - this._error); + MessageMetadata(this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error); } diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index 3097516c..37562aca 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -17,8 +17,9 @@ class FileMessage extends Message { FileMessage(this.metadata, this.content); @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { dynamic shareObj = jsonDecode(this.content); @@ -34,9 +35,7 @@ class FileMessage extends Message { return MessageRow(MalformedBubble()); } - var lrt = Provider.of(bcontext).lastMessageTime; - return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), - key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); + return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize)); }); } diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index fda0e7fe..3b243a05 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -17,8 +17,9 @@ class InviteMessage extends Message { InviteMessage(this.overlay, this.metadata, this.content); @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { String inviteTarget; @@ -40,8 +41,7 @@ class InviteMessage extends Message { } } var lrt = Provider.of(bcontext).lastMessageTime; - return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), - key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); + return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite)); }); } diff --git a/lib/models/messages/malformedmessage.dart b/lib/models/messages/malformedmessage.dart index dc943fe1..ea9896ab 100644 --- a/lib/models/messages/malformedmessage.dart +++ b/lib/models/messages/malformedmessage.dart @@ -9,8 +9,9 @@ class MalformedMessage extends Message { MalformedMessage(this.metadata); @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (context, child) { return MessageRow(MalformedBubble()); diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index fb835989..9fbbc5cc 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -51,7 +51,7 @@ class QuotedMessage extends Message { dynamic message = jsonDecode(this.content); return Text(message["body"]); } 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 - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { try { dynamic message = jsonDecode(this.content); if (message["body"] == null || message["quotedHash"] == null) { - return MalformedMessage(this.metadata).getWidget(context); + return MalformedMessage(this.metadata).getWidget(context, key); } var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]); - int messageIndex = metadata.messageIndex; Future quotedMessage = quotedMessagePotentials.then((matchingMessages) { if (matchingMessages == "[]") { return null; @@ -81,9 +80,7 @@ class QuotedMessage extends Message { // message try { var list = (jsonDecode(matchingMessages) as List).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); - LocallyIndexedMessage candidate = list.reversed.firstWhere((element) => messageIndex < element.index, orElse: () { - return list.firstWhere((element) => messageIndex > element.index); - }); + LocallyIndexedMessage candidate = list.reversed.first; return candidate; } catch (e) { // Malformed Message will be returned... @@ -92,20 +89,18 @@ class QuotedMessage extends Message { }); return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { - var lrt = Provider.of(bcontext).lastMessageTime; - return MessageRow( - QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { - if (localIndex != null) { - return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); - } - return MalformedMessage(this.metadata); - })), - key: Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt)); + return MessageRow(QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { + if (localIndex != null) { + return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); + } + return MalformedMessage(this.metadata); + }))); }); } catch (e) { - return MalformedMessage(this.metadata).getWidget(context); + return MalformedMessage(this.metadata).getWidget(context, key); } } } diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index d1f7fd6b..a94c86f6 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -31,14 +31,15 @@ class TextMessage extends Message { } @override - Widget getWidget(BuildContext context) { + Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( + key: key, value: this.metadata, builder: (bcontext, child) { var lrt = Provider.of(bcontext).lastMessageTime; - var key = Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt); + // var key = Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt); - return MessageRow(MessageBubble(this.content), key: key); + return MessageRow(MessageBubble(this.content)); }); } } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 2b5d225d..7cfd0501 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; +import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/quotedmessage.dart'; @@ -213,6 +214,7 @@ class _MessageViewState extends State { ctrlrCompose.clear(); focusNode.requestFocus(); Future.delayed(const Duration(milliseconds: 80), () { + Provider.of(context, listen: false).contactList.getContact(Provider.of(context, listen: false).identifier)?.bumpMessageCache(); Provider.of(context, listen: false).totalMessages++; Provider.of(context, listen: false).newMarker++; // Resort the contact list... diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index f47b03c9..81fb4533 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -79,13 +79,15 @@ class _MessageListState extends State { var contactHandle = Provider.of(outerContext, listen: false).identifier; var messageIndex = index; + // var key = Provider.of(outerContext, listen: false).getMessageKey(contactHandle, Provider.of(outerContext).totalMessages - index, DateTime.now()); return FutureBuilder( + //key: Provider.of(outerContext, listen: false).getMessageKey(contactHandle, Provider.of(outerContext).totalMessages - index, DateTime.now()), future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data as Message; - // Already includes MessageRow,, - return message.getWidget(context); + var key = Provider.of(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID, DateTime.now()); + return message.getWidget(context, key); } else { return MessageLoadingBubble(); } @@ -97,3 +99,23 @@ class _MessageListState extends State { ]))); } } + +class CachedMessage extends Message { + @override + MessageMetadata getMetadata() { + // TODO: implement getMetadata + throw UnimplementedError(); + } + + @override + Widget getPreviewWidget(BuildContext context) { + // TODO: implement getPreviewWidget + throw UnimplementedError(); + } + + @override + Widget getWidget(BuildContext context, Key key) { + // TODO: implement getWidget + throw UnimplementedError(); + } +} diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 963a41ab..311fc3a1 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -34,7 +34,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi @override void initState() { super.initState(); - index = Provider.of(context, listen: false).messageIndex; + index = Provider.of(context, listen: false).messageID; _controller = AnimationController(vsync: this); _controller.addListener(() { setState(() { @@ -75,7 +75,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi } Widget wdgIcons = Visibility( - visible: Provider.of(context).hoveredIndex == Provider.of(context).messageIndex, + visible: Provider.of(context).hoveredIndex == Provider.of(context).messageID, maintainSize: true, maintainAnimation: true, maintainState: true, @@ -169,7 +169,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi // For desktop... onHover: (event) { setState(() { - Provider.of(context, listen: false).hoveredIndex = Provider.of(context, listen: false).messageIndex; + Provider.of(context, listen: false).hoveredIndex = Provider.of(context, listen: false).messageID; }); }, onExit: (event) { @@ -204,7 +204,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi children: widgetRow, ))))); var mark = Provider.of(context).newMarker; - if (mark > 0 && mark == Provider.of(context).messageIndex + 1) { + if (mark > 0 && Provider.of(context).messageCache[mark]?.metadata.messageID == Provider.of(context).messageID) { return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); } else { return mr; @@ -251,12 +251,17 @@ class MessageRowState extends State with SingleTickerProviderStateMi } void _btnGoto() { - selectConversation(context, Provider.of(context, listen: false).conversationIdentifier); + var id = Provider.of(context, listen: false).contactList.findContact(Provider.of(context, listen: false).senderHandle)?.identifier; + if (id == null) { + // Can't happen + } else { + selectConversation(context, id); + } } void _btnAdd() { var sender = Provider.of(context, listen: false).senderHandle; - if (sender == null || sender == "") { + if (sender == "") { print("sender not yet loaded"); return; } From 995282fa04d798936252f51732eb5b3d855c9473 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Dec 2021 13:42:40 -0800 Subject: [PATCH 16/28] Fixup Acks / Message Keys --- lib/cwtch/cwtchNotifier.dart | 18 +++++++----------- lib/model.dart | 9 +++++++-- lib/models/message.dart | 4 ++-- lib/models/messages/filemessage.dart | 2 +- lib/models/messages/invitemessage.dart | 5 ++--- lib/models/messages/malformedmessage.dart | 3 +-- lib/models/messages/quotedmessage.dart | 15 ++++++++------- lib/models/messages/textmessage.dart | 9 ++++----- lib/widgets/messagerow.dart | 4 +++- 9 files changed, 35 insertions(+), 34 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 72d68f26..185ed735 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -35,7 +35,7 @@ class CwtchNotifier { } void handleMessage(String type, dynamic data) { - EnvironmentConfig.debugLog("NewEvent $type $data"); + //EnvironmentConfig.debugLog("NewEvent $type $data"); switch (type) { case "CwtchStarted": appState.SetCwtchInit(); @@ -144,7 +144,7 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; } profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"], ""); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; // We only ever see messages from authenticated peers. @@ -170,17 +170,17 @@ class CwtchNotifier { if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); - if (message == null) break; + message.ackd = true; + // 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 // end during syncing. if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.isOnline() == false) { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.status = "Authenticated"; } - message.ackd = true; + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation)!.ackCache(messageID); } catch (e) { - // ignore, we received an ack for a message that hasn't loaded onto the screen yet... - // the protocol was faster than the ui....yay? + // ignore, most likely cause is the key got optimized out... } break; case "NewMessageFromGroup": @@ -194,11 +194,7 @@ class CwtchNotifier { // Only bother to do anything if we know about the group and the provided index is greater than our current total... if (currentTotal != null && idx >= currentTotal) { - profileCN - .getProfile(data["ProfileOnion"]) - ?.contactList - .getContact(identifier)! - .updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"], data["Signature"]); + profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; //if not currently open diff --git a/lib/model.dart b/lib/model.dart index f08113cf..4c94fb8e 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -687,11 +687,16 @@ class ContactInfoState extends ChangeNotifier { return ret; } - void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data, String signature) { - this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, signature, {}, false, false), data)); + void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, String data) { + this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false), data)); } void bumpMessageCache() { this.messageCache.insert(0, null); } + + void ackCache(int messageID) { + this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true; + notifyListeners(); + } } diff --git a/lib/models/message.dart b/lib/models/message.dart index 0fab66e9..4df9eace 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -58,8 +58,8 @@ Message compileOverlay(MessageMetadata metadata, String messageData) { } Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) { - var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)!.messageCache; - if (cache.length > index) { + var cache = Provider.of(context).contactList.getContact(conversationIdentifier)?.messageCache; + if (cache != null && cache.length > index) { if (cache[index] != null) { return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper)); } diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index 37562aca..9254c6d2 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -35,7 +35,7 @@ class FileMessage extends Message { return MessageRow(MalformedBubble()); } - return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize)); + return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: key); }); } diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index 3b243a05..149ba5e0 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -37,11 +37,10 @@ class InviteMessage extends Message { inviteTarget = jsonObj['GroupID']; inviteNick = jsonObj['GroupName']; } else { - return MessageRow(MalformedBubble()); + return MessageRow(MalformedBubble(), key: key); } } - var lrt = Provider.of(bcontext).lastMessageTime; - return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite)); + return MessageRow(InvitationBubble(overlay, inviteTarget, inviteNick, invite), key: key); }); } diff --git a/lib/models/messages/malformedmessage.dart b/lib/models/messages/malformedmessage.dart index ea9896ab..0ebf2281 100644 --- a/lib/models/messages/malformedmessage.dart +++ b/lib/models/messages/malformedmessage.dart @@ -11,10 +11,9 @@ class MalformedMessage extends Message { @override Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( - key: key, value: this.metadata, builder: (context, child) { - return MessageRow(MalformedBubble()); + return MessageRow(MalformedBubble(), key: key); }); } diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 9fbbc5cc..5a69ca91 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -89,15 +89,16 @@ class QuotedMessage extends Message { }); return ChangeNotifierProvider.value( - key: key, value: this.metadata, builder: (bcontext, child) { - return MessageRow(QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { - if (localIndex != null) { - return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); - } - return MalformedMessage(this.metadata); - }))); + return MessageRow( + QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { + if (localIndex != null) { + return messageHandler(context, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); + } + return MalformedMessage(this.metadata); + })), + key: key); }); } catch (e) { return MalformedMessage(this.metadata).getWidget(context, key); diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index a94c86f6..a8d7f6af 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -33,13 +33,12 @@ class TextMessage extends Message { @override Widget getWidget(BuildContext context, Key key) { return ChangeNotifierProvider.value( - key: key, value: this.metadata, builder: (bcontext, child) { - var lrt = Provider.of(bcontext).lastMessageTime; - // var key = Provider.of(bcontext).getMessageKey(this.metadata.conversationIdentifier, this.metadata.messageID, lrt); - - return MessageRow(MessageBubble(this.content)); + return MessageRow( + MessageBubble(this.content), + key: key, + ); }); } } diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 311fc3a1..55884aa2 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -204,7 +204,9 @@ class MessageRowState extends State with SingleTickerProviderStateMi children: widgetRow, ))))); var mark = Provider.of(context).newMarker; - if (mark > 0 && Provider.of(context).messageCache[mark]?.metadata.messageID == Provider.of(context).messageID) { + if (mark > 0 && + Provider.of(context).messageCache.length > mark && + Provider.of(context).messageCache[mark]?.metadata.messageID == Provider.of(context).messageID) { return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); } else { return mr; From 35d09a5207d9a592b25accdf65d54003f16c3b26 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 7 Dec 2021 21:46:39 -0800 Subject: [PATCH 17/28] Upgrade LibCwtch-go --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index 462defe0..3c6d2eca 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2021-11-09-18-25-v1.4.2 \ No newline at end of file +2021-12-08-00-32-v1.5.0-7-g28a13aa \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 9246eb36..33bf0526 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2021-11-09-23-25-v1.4.2 \ No newline at end of file +2021-12-08-05-32-v1.5.0-7-g28a13aa \ No newline at end of file From 6b4ccd401eedd474789f1b8bdb628881f3a13f20 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 7 Dec 2021 21:47:28 -0800 Subject: [PATCH 18/28] Flutter Upgrades --- linux/flutter/generated_plugin_registrant.cc | 4 ++++ linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 4 ++-- windows/flutter/generated_plugin_registrant.cc | 3 +++ windows/flutter/generated_plugins.cmake | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d2..f6f23bfe 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include 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); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 51436ae8..1fc8ed34 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index edb8e3de..e990cfd9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,7 +417,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.7" typed_data: dependency: transitive description: @@ -473,7 +473,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" win32: dependency: transitive description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d4680..4f788487 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4d10c251..411af46d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) set(PLUGIN_BUNDLED_LIBRARIES) From 30fa788b8411e367272a3302f3c103aeb5be8422 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 8 Dec 2021 14:38:29 -0800 Subject: [PATCH 19/28] Combine update cache and total messages increase. Remove CachedMessage --- lib/cwtch/cwtchNotifier.dart | 10 ---------- lib/model.dart | 2 ++ lib/views/messageview.dart | 1 - lib/widgets/messagelist.dart | 22 ---------------------- 4 files changed, 2 insertions(+), 33 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 185ed735..d24aa00b 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -145,7 +145,6 @@ class CwtchNotifier { } profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, data["Data"]); - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; // 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 @@ -195,7 +194,6 @@ class CwtchNotifier { // Only bother to do anything if we know about the group and the provided index is greater than our current total... if (currentTotal != null && idx >= currentTotal) { profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, data["Data"]); - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages++; //if not currently open if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { @@ -221,14 +219,6 @@ class CwtchNotifier { // This is not dealt with by IndexedAcknowledgment } 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.findContact(contactHandle)!.totalMessages) { - profileCN.getProfile(data["Identity"])?.contactList.findContact(contactHandle)!.totalMessages = total; - } - break; case "SendMessageToPeerError": // Ignore break; diff --git a/lib/model.dart b/lib/model.dart index 4c94fb8e..eb5da8c9 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -689,10 +689,12 @@ class ContactInfoState extends ChangeNotifier { 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) { diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 7cfd0501..539bebfb 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -215,7 +215,6 @@ class _MessageViewState extends State { focusNode.requestFocus(); Future.delayed(const Duration(milliseconds: 80), () { Provider.of(context, listen: false).contactList.getContact(Provider.of(context, listen: false).identifier)?.bumpMessageCache(); - Provider.of(context, listen: false).totalMessages++; Provider.of(context, listen: false).newMarker++; // Resort the contact list... Provider.of(context, listen: false).contactList.updateLastMessageTime(Provider.of(context, listen: false).identifier, DateTime.now()); diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 81fb4533..e090e3ac 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -79,9 +79,7 @@ class _MessageListState extends State { var contactHandle = Provider.of(outerContext, listen: false).identifier; var messageIndex = index; - // var key = Provider.of(outerContext, listen: false).getMessageKey(contactHandle, Provider.of(outerContext).totalMessages - index, DateTime.now()); return FutureBuilder( - //key: Provider.of(outerContext, listen: false).getMessageKey(contactHandle, Provider.of(outerContext).totalMessages - index, DateTime.now()), future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), builder: (context, snapshot) { if (snapshot.hasData) { @@ -99,23 +97,3 @@ class _MessageListState extends State { ]))); } } - -class CachedMessage extends Message { - @override - MessageMetadata getMetadata() { - // TODO: implement getMetadata - throw UnimplementedError(); - } - - @override - Widget getPreviewWidget(BuildContext context) { - // TODO: implement getPreviewWidget - throw UnimplementedError(); - } - - @override - Widget getWidget(BuildContext context, Key key) { - // TODO: implement getWidget - throw UnimplementedError(); - } -} From c5c0f21829c8d8f91e8b8cdfac3632ccd7dd4dd1 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 8 Dec 2021 16:41:01 -0800 Subject: [PATCH 20/28] Fix New Marker. Add Placeholder for Import Handling --- lib/cwtch/cwtchNotifier.dart | 3 +++ lib/model.dart | 2 +- lib/widgets/messagerow.dart | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index d24aa00b..aeae4aa2 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cwtch/main.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; @@ -365,6 +366,8 @@ class CwtchNotifier { case "FileDownloaded": profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]); break; + case "ImportingProfileEvent": + break; default: EnvironmentConfig.debugLog("unhandled event: $type"); } diff --git a/lib/model.dart b/lib/model.dart index eb5da8c9..7633688a 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -631,7 +631,7 @@ class ContactInfoState extends ChangeNotifier { set newMarker(int newVal) { // only unreadMessages++ can set newMarker = 1; // avoids drawing a marker when the convo is already open - if (newVal > 1) { + if (newVal >= 1) { this._newMarker = newVal; notifyListeners(); } diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 55884aa2..9941fb52 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -206,7 +206,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi var mark = Provider.of(context).newMarker; if (mark > 0 && Provider.of(context).messageCache.length > mark && - Provider.of(context).messageCache[mark]?.metadata.messageID == Provider.of(context).messageID) { + Provider.of(context).messageCache[mark - 1]?.metadata.messageID == Provider.of(context).messageID) { return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); } else { return mr; From 066b4d4dece1c3f1b4b01c26f7996d5d3b54e2ec Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 10 Dec 2021 11:51:19 -0800 Subject: [PATCH 21/28] Erinn Comment Fixups --- .../app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 8 ++++---- .../app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt | 6 ------ lib/cwtch/cwtchNotifier.dart | 8 ++++---- lib/model.dart | 9 ++++----- lib/widgets/messagelist.dart | 2 +- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 1ab53ec9..e8c737ad 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -133,10 +133,10 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : notificationManager.notify(dlID, newNotification); } } catch (e: Exception) { - Log.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace()); + Log.d("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace()); } } else if (evt.EventType == "FileDownloaded") { - Log.i("FlwtchWorker", "file downloaded!"); + Log.d("FlwtchWorker", "file downloaded!"); val data = JSONObject(evt.Data); val tempFile = data.getString("TempFile"); val fileKey = data.getString("FileKey"); @@ -147,7 +147,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val targetUri = Uri.parse(filePath); val os = this.applicationContext.getContentResolver().openOutputStream(targetUri); val bytesWritten = Files.copy(sourcePath, os); - Log.i("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes"); + Log.d("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes"); if (bytesWritten != 0L) { os?.flush(); os?.close(); @@ -184,7 +184,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val profile = (a.get("ProfileOnion") as? String) ?: "" val conversation = a.getInt("conversation").toLong() val indexI = a.getInt("index").toLong() - Log.i("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString()) + Log.d("FlwtchWorker", "Cwtch GetMessage " + profile + " " + conversation.toString() + " " + indexI.toString()) return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, conversation, indexI)).build()) } "GetMessageByID" -> { diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index dec5f220..20940c89 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -30,7 +30,6 @@ import android.os.Build import android.os.Environment import android.database.Cursor import android.provider.MediaStore -import cwtch.Cwtch class MainActivity: FlutterActivity() { override fun provideSplashScreen(): SplashScreen? = SplashView() @@ -148,11 +147,6 @@ class MainActivity: FlutterActivity() { // that we can divert this method call to ReconnectCwtchForeground instead if so. val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get() for (workInfo in works) { - Log.i("handleCwtch:WorkManager", "$workInfo") - 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).cancelWorkById(workInfo.id) } WorkManager.getInstance(this).pruneWork() diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index aeae4aa2..d3c01c55 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -166,7 +166,7 @@ class CwtchNotifier { var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(conversation); // We return -1 for protocol message acks if there is no message if (messageID == -1) break; - var key = contact!.getMessageKeyOrFail(conversation, messageID, contact.lastMessageTime); + var key = contact!.getMessageKeyOrFail(conversation, messageID); if (key == null) break; try { var message = Provider.of(key.currentContext!, listen: false); @@ -217,7 +217,8 @@ class CwtchNotifier { notificationManager.notify("New Message From Group!"); } } else { - // This is not dealt with by IndexedAcknowledgment + // This is dealt with by IndexedAcknowledgment + EnvironmentConfig.debugLog("new message from group from yourself - this should not happen"); } break; case "SendMessageToPeerError": @@ -226,7 +227,7 @@ class CwtchNotifier { case "IndexedFailure": var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]); var idx = int.parse(data["Index"]); - var key = contact?.getMessageKeyOrFail(contact.identifier, idx, contact.lastMessageTime); + var key = contact?.getMessageKeyOrFail(contact.identifier, idx); if (key != null) { var message = Provider.of(key.currentContext!, listen: false); message.error = true; @@ -301,7 +302,6 @@ class CwtchNotifier { break; case "ServerStateChange": // Update the Server Cache - //EnvironmentConfig.debugLog("server state changes $data"); profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]); profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) { if (contact.isGroup == true && contact.server == data["GroupServer"]) { diff --git a/lib/model.dart b/lib/model.dart index 7633688a..87ac2f36 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -667,9 +667,8 @@ class ContactInfoState extends ChangeNotifier { } } - GlobalKey getMessageKey(int conversation, int message, DateTime lastread) { - String index = "c: " + conversation.toString() + " m:" + message.toString(); //+ " lr:" +lastMessageTime.toIso8601String(); - //EnvironmentConfig.debugLog("looked up key $index"); + GlobalKey getMessageKey(int conversation, int message) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); if (keys[index] == null) { keys[index] = GlobalKey(); } @@ -677,8 +676,8 @@ class ContactInfoState extends ChangeNotifier { return ret; } - GlobalKey? getMessageKeyOrFail(int conversation, int message, DateTime lastread) { - String index = "c: " + conversation.toString() + " m:" + message.toString(); // + " lr:" +lastMessageTime.toIso8601String(); + GlobalKey? getMessageKeyOrFail(int conversation, int message) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); if (keys[index] == null) { return null; diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index e090e3ac..365a75fc 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -84,7 +84,7 @@ class _MessageListState extends State { builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data as Message; - var key = Provider.of(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID, DateTime.now()); + var key = Provider.of(outerContext, listen: false).getMessageKey(contactHandle, message.getMetadata().messageID); return message.getWidget(context, key); } else { return MessageLoadingBubble(); From 1d5359e645c44ab5a6812d9d68acebde483cd7b6 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 9 Nov 2021 15:27:26 -0800 Subject: [PATCH 22/28] start of profile server manager --- lib/models/profileservers.dart | 7 +++-- lib/views/contactsview.dart | 14 +++++++-- lib/views/profileServersView.dart | 50 +++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 lib/views/profileServersView.dart diff --git a/lib/models/profileservers.dart b/lib/models/profileservers.dart index cb86d392..51a1a34e 100644 --- a/lib/models/profileservers.dart +++ b/lib/models/profileservers.dart @@ -14,10 +14,10 @@ class ProfileServerListState extends ChangeNotifier { return idx >= 0 ? _servers[idx] : null; } - void updateServerCache(String onion, String status) { + void updateServerCache(String onion, String description, String status) { int idx = _servers.indexWhere((element) => element.onion == onion); if (idx >= 0) { - _servers[idx] = RemoteServerInfoState(onion: onion, status: status); + _servers[idx] = RemoteServerInfoState(onion: onion, description: description, status: status); } else { print("Tried to update server cache without a starting state...this is probably an error"); } @@ -31,6 +31,7 @@ class ProfileServerListState extends ChangeNotifier { class RemoteServerInfoState extends ChangeNotifier { final String onion; final String status; + final String description; - RemoteServerInfoState({required this.onion, required this.status}); + RemoteServerInfoState({required this.onion, required this.description, required this.status}); } diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 990899f3..9a19d982 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -112,7 +112,15 @@ class _ContactsViewState extends State { Clipboard.setData(new ClipboardData(text: Provider.of(context, listen: false).onion)); })); - // TODO servers + // Manage servers + if (Provider.of(context, listen: false).isExperimentEnabled(ServerManagementExperiment)) { + actions.add(IconButton( + icon: Icon(CwtchIcons.dns_24px), + tooltip: "Manage known servers", //AppLocalizations.of(context)!.copyAddress, + onPressed: () { + _pushServers(); + })); + } // Search contacts actions.add(IconButton( @@ -162,12 +170,12 @@ class _ContactsViewState extends State { )); } - void _pushTorStatus() { + void _pushServers() { Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return MultiProvider( providers: [Provider.value(value: Provider.of(context))], - child: TorStatusView(), + child: ProfileServersView(), ); }, )); diff --git a/lib/views/profileServersView.dart b/lib/views/profileServersView.dart new file mode 100644 index 00000000..2b9b99c9 --- /dev/null +++ b/lib/views/profileServersView.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + + +class ProfileServersView extends StatefulWidget { + @override + _ProfileServersView createState() => _ProfileServersView(); +} + +class _ProfileServersView extends State { + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), + //actions: getActions(), + ), + body: Consumer( + builder: (context, svrs, child) { + final tiles = svrs.servers.map((ServerInfoState server) { + return ChangeNotifierProvider.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); + }, + )); + } \ No newline at end of file From 0c797faf05300773687bde388bba9199fbe14c3d Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Thu, 18 Nov 2021 16:09:55 -0800 Subject: [PATCH 23/28] profile level server list and editor start --- lib/model.dart | 31 +++++- lib/models/profileservers.dart | 60 ++++++++++- lib/views/addeditservers.dart | 2 +- lib/views/contactsview.dart | 4 +- lib/views/profileServersView.dart | 50 --------- lib/views/profileserversview.dart | 59 +++++++++++ lib/views/remoteserverview.dart | 167 ++++++++++++++++++++++++++++++ lib/widgets/buttontextfield.dart | 5 +- lib/widgets/remoteserverrow.dart | 77 ++++++++++++++ lib/widgets/serverrow.dart | 6 +- 10 files changed, 400 insertions(+), 61 deletions(-) delete mode 100644 lib/views/profileServersView.dart create mode 100644 lib/views/profileserversview.dart create mode 100644 lib/views/remoteserverview.dart create mode 100644 lib/widgets/remoteserverrow.dart diff --git a/lib/model.dart b/lib/model.dart index 87ac2f36..d2f68bda 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -120,6 +120,7 @@ class ProfileListState extends ChangeNotifier { } class ContactListState extends ChangeNotifier { + ProfileServerListState? servers; List _contacts = []; String _filter = ""; int get num => _contacts.length; @@ -131,18 +132,36 @@ class ContactListState extends ChangeNotifier { notifyListeners(); } + void connectServers(ProfileServerListState servers) { + this.servers = servers; + } + List filteredList() { if (!isFiltered) return contacts; return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList(); } void addAll(Iterable newContacts) { + print("****** contactListState.addAll()... *********"); _contacts.addAll(newContacts); + servers?.clearGroups(); + print("contact len: ${_contacts.length}"); + _contacts.forEach((contact) { + //print("looking at contact ${contact.onion} (${contact.isGroup})..."); + if (contact.isGroup) { + print("contactList adding group ${contact.onion} to ${contact.server}"); + servers?.addGroup(contact); + } + }); notifyListeners(); } void add(ContactInfoState newContact) { _contacts.add(newContact); + if (newContact.isGroup) { + print("contactList adding group ${newContact.onion} to ${newContact.server}"); + servers?.addGroup(newContact); + } notifyListeners(); } @@ -213,8 +232,8 @@ class ContactListState extends ChangeNotifier { } class ProfileInfoState extends ChangeNotifier { - ContactListState _contacts = ContactListState(); ProfileServerListState _servers = ProfileServerListState(); + ContactListState _contacts = ContactListState(); final String onion; String _nickname = ""; String _imagePath = ""; @@ -242,7 +261,11 @@ class ProfileInfoState extends ChangeNotifier { this._online = online; this._encrypted = encrypted; + _contacts.connectServers(this._servers); + if (contactsJson != null && contactsJson != "" && contactsJson != "null") { + this.replaceServers(serversJson); + List contacts = jsonDecode(contactsJson); this._contacts.addAll(contacts.map((contact) { return ContactInfoState(this.onion, contact["identifier"], contact["onion"], @@ -265,7 +288,7 @@ class ProfileInfoState extends ChangeNotifier { } } - this.replaceServers(serversJson); + } // Parse out the server list json into our server info state struct... @@ -274,7 +297,7 @@ class ProfileInfoState extends ChangeNotifier { List servers = jsonDecode(serversJson); this._servers.replace(servers.map((server) { // TODO Keys... - return RemoteServerInfoState(onion: server["onion"], status: server["status"]); + return RemoteServerInfoState(onion: server["onion"], description: server["description"], status: server["status"]); })); notifyListeners(); } @@ -282,7 +305,7 @@ class ProfileInfoState extends ChangeNotifier { // void updateServerStatusCache(String server, String status) { - this._servers.updateServerCache(server, status); + this._servers.updateServerState(server, status); notifyListeners(); } diff --git a/lib/models/profileservers.dart b/lib/models/profileservers.dart index 51a1a34e..ebd1cf99 100644 --- a/lib/models/profileservers.dart +++ b/lib/models/profileservers.dart @@ -1,3 +1,4 @@ +import 'package:cwtch/model.dart'; import 'package:flutter/material.dart'; class ProfileServerListState extends ChangeNotifier { @@ -6,6 +7,7 @@ class ProfileServerListState extends ChangeNotifier { void replace(Iterable newServers) { _servers.clear(); _servers.addAll(newServers); + resort(); notifyListeners(); } @@ -14,16 +16,43 @@ class ProfileServerListState extends ChangeNotifier { return idx >= 0 ? _servers[idx] : null; } - void updateServerCache(String onion, String description, String status) { + void updateServerState(String onion, String status) { int idx = _servers.indexWhere((element) => element.onion == onion); if (idx >= 0) { - _servers[idx] = RemoteServerInfoState(onion: onion, description: description, status: status); + _servers[idx] = RemoteServerInfoState(onion: onion, description: _servers[idx].description, status: status); } else { print("Tried to update server cache without a starting state...this is probably an error"); } + resort(); notifyListeners(); } + void resort() { + _servers.sort((RemoteServerInfoState a, RemoteServerInfoState b) { + // return -1 = a first in list + // return 1 = b first in list + if (a.status == "Synced" && b.status != "Synced") { + return -1; + } else if (a.status != "Synced" && b.status == "Synced") { + return 1; + } + return 0; + }); + } + + void clearGroups() { + _servers.map((server) => server.clearGroups()); + } + + void addGroup(ContactInfoState group) { + print("serverList adding group ${group.onion} to ${group.server}"); + + int idx = _servers.indexWhere((element) => element.onion == group.server); + if (idx >= 0) { + _servers[idx].addGroup(group); + } + } + List get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier } @@ -31,7 +60,32 @@ class ProfileServerListState extends ChangeNotifier { class RemoteServerInfoState extends ChangeNotifier { final String onion; final String status; - final String description; + String description; + List _groups = []; RemoteServerInfoState({required this.onion, required this.description, required this.status}); + + void updateDescription(String newDescription) { + this.description = newDescription; + notifyListeners(); + } + + void clearGroups() { + print("Server CLEARING group"); + description = "cleared groups"; + _groups = []; + } + + void addGroup(ContactInfoState group) { + print("server $onion adding group ${group.onion}"); + _groups.add(group); + print("now has ${_groups.length}"); + description = "i have ${_groups.length} groups"; + notifyListeners(); + } + + int get groupsLen => _groups.length; + + List get groups => _groups.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier + } diff --git a/lib/views/addeditservers.dart b/lib/views/addeditservers.dart index 2a1c8f46..b2a5cacf 100644 --- a/lib/views/addeditservers.dart +++ b/lib/views/addeditservers.dart @@ -110,7 +110,7 @@ class _AddEditServerViewState extends State { ), CwtchTextField( controller: ctrlrDesc, - labelText: "Description", + labelText: AppLocalizations.of(context)!.fieldDescriptionLabel, autofocus: false, ) ]), diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 9a19d982..af776607 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -1,4 +1,5 @@ import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/views/profileserversview.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/views/torstatusview.dart'; import 'package:cwtch/widgets/contactrow.dart'; @@ -171,10 +172,11 @@ class _ContactsViewState extends State { } void _pushServers() { + var profile = Provider.of(context); Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return MultiProvider( - providers: [Provider.value(value: Provider.of(context))], + providers: [ChangeNotifierProvider(create: (context) => profile.serverList), Provider.value(value: Provider.of(context))], child: ProfileServersView(), ); }, diff --git a/lib/views/profileServersView.dart b/lib/views/profileServersView.dart deleted file mode 100644 index 2b9b99c9..00000000 --- a/lib/views/profileServersView.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - - -class ProfileServersView extends StatefulWidget { - @override - _ProfileServersView createState() => _ProfileServersView(); -} - -class _ProfileServersView extends State { - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), - //actions: getActions(), - ), - body: Consumer( - builder: (context, svrs, child) { - final tiles = svrs.servers.map((ServerInfoState server) { - return ChangeNotifierProvider.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); - }, - )); - } \ No newline at end of file diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart new file mode 100644 index 00000000..aad94f85 --- /dev/null +++ b/lib/views/profileserversview.dart @@ -0,0 +1,59 @@ +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 '../model.dart'; + + +class ProfileServersView extends StatefulWidget { + @override + _ProfileServersView createState() => _ProfileServersView(); +} + +class _ProfileServersView extends State { + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: Text(MediaQuery + .of(context) + .size + .width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), + //actions: getActions(), + ), + body: Consumer( + builder: (context, servers, child) { + final tiles = servers.servers.map((RemoteServerInfoState server) { + return ChangeNotifierProvider.value( + value: server, + builder: (context, child) => RepaintBoundary(child: RemoteServerRow()), + ); + }, + ); + + final divided = ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList(); + + // TODO: add import row from global servers + divided.insert(0, Row( children: [Text("Import server from global list if any")])); + + return ListView(children: divided); + }, + )); + } + + + +} \ No newline at end of file diff --git a/lib/views/remoteserverview.dart b/lib/views/remoteserverview.dart new file mode 100644 index 00000000..22bee1bf --- /dev/null +++ b/lib/views/remoteserverview.dart @@ -0,0 +1,167 @@ +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 { + final _formKey = GlobalKey(); + + final ctrlrDesc = TextEditingController(text: ""); + + @override + void initState() { + super.initState(); + var serverInfoState = Provider.of(context, listen: false); + if (serverInfoState.description.isNotEmpty) { + ctrlrDesc.text = serverInfoState.description; + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer2(builder: (context, serverInfoState, settings, child) { + return Scaffold( + appBar: AppBar( + title: Text(ctrlrDesc.text.isNotEmpty ? ctrlrDesc.text : serverInfoState.onion) + ), + body: LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { + return Scrollbar( + isAlwaysShown: true, + child: SingleChildScrollView( + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Form( + key: _formKey, + child: Container( + margin: EdgeInsets.fromLTRB(30, 0, 30, 10), + padding: EdgeInsets.fromLTRB(20, 0, 20, 10), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context)!.serverAddress), + SizedBox( + height: 20, + ), + SelectableText( + serverInfoState.onion + ) + ]), + + // Description + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel), + Text(AppLocalizations.of(context)!.serverDescriptionDescription), + SizedBox( + height: 20, + ), + CwtchButtonTextField( + controller: ctrlrDesc, + readonly: false, + tooltip: "Save", //TODO localize + labelText: "Description", // TODO localize + icon: Icon(Icons.save), + onPressed: () { + // TODO save + }, + ) + ]), + + Text("Groups on this server"), + _buildGroupsList(serverInfoState), + + ])))))); + }),); + }); + } + + Widget _buildGroupsList(RemoteServerInfoState serverInfoState) { + print("groups: ${serverInfoState.groups} lenMethod: ${serverInfoState.groupsLen} len: ${serverInfoState.groups.length}"); + final tiles = serverInfoState.groups.map((ContactInfoState group) { + print("building group tile for ${group.onion}"); + return ChangeNotifierProvider.value(key: ValueKey(group.profileOnion + "" + group.onion), value: group, builder: (_, __) => RepaintBoundary(child: _buildGroupRow(group))); + }); + final divided = ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList(); + return RepaintBoundary(child: ListView(children: divided)); + } + + void _savePressed() { + + var server = Provider.of(context, listen: false); + + Provider.of(context, listen: false) + .cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text); + server.setDescription(ctrlrDesc.text); + + + if (_formKey.currentState!.validate()) { + // TODO support change password + } + Navigator.of(context).pop(); + } + + Widget _buildGroupRow(ContactInfoState group) { + return Column( + children: [ + Text( + group.nickname, + style: Provider.of(context).biggerFont.apply(color: Provider.of(context).theme.portraitOnlineBorderColor()), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + Visibility( + visible: !Provider.of(context).streamerMode, + child: ExcludeSemantics( + child: Text( + group.onion, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Provider.of(context).theme.portraitOnlineBorderColor()), + ))) + ], + ); + } + +} + diff --git a/lib/widgets/buttontextfield.dart b/lib/widgets/buttontextfield.dart index 46e88796..cd1cdb09 100644 --- a/lib/widgets/buttontextfield.dart +++ b/lib/widgets/buttontextfield.dart @@ -5,12 +5,13 @@ import 'package:provider/provider.dart'; // Provides a styled Text Field for use in Form Widgets. // Callers must provide a text controller, label helper text and a validator. 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 Function()? onPressed; final Icon icon; final String tooltip; final bool readonly; + String? labelText; @override _CwtchButtonTextFieldState createState() => _CwtchButtonTextFieldState(); @@ -39,6 +40,8 @@ class _CwtchButtonTextFieldState extends State { focusNode: _focusNode, enableIMEPersonalizedLearning: false, decoration: InputDecoration( + labelText: widget.labelText, + labelStyle: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()), suffixIcon: IconButton( onPressed: widget.onPressed, icon: widget.icon, diff --git a/lib/widgets/remoteserverrow.dart b/lib/widgets/remoteserverrow.dart new file mode 100644 index 00000000..0df655d2 --- /dev/null +++ b/lib/widgets/remoteserverrow.dart @@ -0,0 +1,77 @@ +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 { + @override + Widget build(BuildContext context) { + var server = Provider.of(context); + var description = server.description.isNotEmpty ? server.description : server.onion; + var running = server.status == "Synced"; + 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(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), + size: 64) + + ), + Expanded( + child: Column( + children: [ + Text( + description, + semanticsLabel: description, + style: Provider.of(context).biggerFont.apply(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + Visibility( + visible: !Provider.of(context).streamerMode, + child: ExcludeSemantics( + child: Text( + server.onion, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + ))) + ], + )), + + ]), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + settings: RouteSettings(name: "remoteserverview"), + builder: (BuildContext context) { + return MultiProvider( + providers: [ChangeNotifierProvider(create: (context) => server), Provider.value(value: Provider.of(context))], + child: RemoteServerView(), + ); + })); + } + )); + } + +} \ No newline at end of file diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart index 9c12f477..a4e8bb05 100644 --- a/lib/widgets/serverrow.dart +++ b/lib/widgets/serverrow.dart @@ -73,7 +73,11 @@ class _ServerRowState extends State { _pushEditServer(server); }, ) - ]))); + ]), + onTap: () { + _pushEditServer(server); + } + )); } void _pushEditServer(ServerInfoState server) { From 3f5428eff881dcbb42f946500e444faf425e1c1a Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Sat, 20 Nov 2021 10:09:06 -0800 Subject: [PATCH 24/28] complete profile level server managment --- lib/l10n/intl_de.arb | 15 ++- lib/l10n/intl_en.arb | 15 ++- lib/l10n/intl_es.arb | 15 ++- lib/l10n/intl_fr.arb | 19 +++- lib/l10n/intl_it.arb | 135 ++++++++++++------------ lib/l10n/intl_pl.arb | 123 +++++++++++----------- lib/l10n/intl_pt.arb | 15 ++- lib/model.dart | 12 ++- lib/models/profileservers.dart | 23 +++-- lib/views/addeditprofileview.dart | 3 +- lib/views/contactsview.dart | 6 +- lib/views/profileserversview.dart | 98 +++++++++++++++++- lib/views/remoteserverview.dart | 164 ++++++++++++++---------------- lib/widgets/remoteserverrow.dart | 85 ++++++++-------- 14 files changed, 433 insertions(+), 295 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index b3793cca..b08f6de8 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,16 @@ { "@@locale": "de", - "@@last_modified": "2021-11-11T01:02:08+01:00", + "@@last_modified": "2021-11-21T17:42:07+01:00", + "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": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.", "newMessagesLabel": "New Messages", "localeRU": "Russian", "copyServerKeys": "Copy keys", @@ -55,7 +65,6 @@ "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", @@ -190,7 +199,6 @@ "radioNoPassword": "Unverschlüsselt (kein Passwort)", "radioUsePassword": "Passwort", "copiedToClipboardNotification": "in die Zwischenablage kopiert", - "copyBtn": "Kopieren", "editProfile": "Profil bearbeiten", "newProfile": "Neues Profil", "defaultProfileName": "Alice", @@ -210,6 +218,7 @@ "acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen", "newGroupBtn": "Neue Gruppe anlegen", "copiedClipboardNotification": "in die Zwischenablage kopiert", + "copyBtn": "Kopieren", "pendingLabel": "Bestätigung ausstehend", "acknowledgedLabel": "bestätigt", "couldNotSendMsgError": "Nachricht konnte nicht gesendet werden", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 70f13992..4069f313 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,16 @@ { "@@locale": "en", - "@@last_modified": "2021-11-11T01:02:08+01:00", + "@@last_modified": "2021-11-21T17:42:07+01:00", + "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", "localeRU": "Russian", "copyServerKeys": "Copy keys", @@ -55,7 +65,6 @@ "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", @@ -190,7 +199,6 @@ "radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "Password", "copiedToClipboardNotification": "Copied to Clipboard", - "copyBtn": "Copy", "editProfile": "Edit Profille", "newProfile": "New Profile", "defaultProfileName": "Alice", @@ -210,6 +218,7 @@ "acceptGroupInviteLabel": "Do you want to accept the invitation to", "newGroupBtn": "Create new group", "copiedClipboardNotification": "Copied to clipboard", + "copyBtn": "Copy", "pendingLabel": "Pending", "acknowledgedLabel": "Acknowledged", "couldNotSendMsgError": "Could not send this message", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index ba0a54bc..bb62828f 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,16 @@ { "@@locale": "es", - "@@last_modified": "2021-11-11T01:02:08+01:00", + "@@last_modified": "2021-11-21T17:42:07+01:00", + "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": "Determina si eliminar o no el historial asociado con el contacto.", "newMessagesLabel": "New Messages", "localeRU": "Russian", "copyServerKeys": "Copy keys", @@ -55,7 +65,6 @@ "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", @@ -190,7 +199,6 @@ "radioNoPassword": "Sin cifrado (sin contraseña)", "radioUsePassword": "Contraseña", "copiedToClipboardNotification": "Copiado al portapapeles", - "copyBtn": "Copiar", "editProfile": "Editar perfil", "newProfile": "Nuevo perfil", "defaultProfileName": "Alicia", @@ -210,6 +218,7 @@ "acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ", "newGroupBtn": "Crear un nuevo grupo de chat", "copiedClipboardNotification": "Copiado al portapapeles", + "copyBtn": "Copiar", "pendingLabel": "Pendiente", "acknowledgedLabel": "Reconocido", "couldNotSendMsgError": "No se pudo enviar este mensaje", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index bb6e8810..fdd9808f 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,8 +1,18 @@ { "@@locale": "fr", - "@@last_modified": "2021-11-11T01:02:08+01:00", - "newMessagesLabel": "New Messages", - "localeRU": "Russian", + "@@last_modified": "2021-11-21T17:42:07+01:00", + "manageKnownServersShort": "Servers", + "manageKnownServersLong": "Manage Known Servers", + "displayNameTooltip": "Veuillez entrer un nom d'usage s'il vous plaît", + "manageKnownServersButton": "Gérer les serveurs connus", + "fieldDescriptionLabel": "Description", + "groupsOnThisServerLabel": "Les groupes dont je fais partie sont hébergés sur ce serveur", + "importLocalServerButton": "Importer %1", + "importLocalServerSelectText": "Sélectionnez le serveur local", + "importLocalServerLabel": "Importer un serveur hébergé localement", + "savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au contact.", + "newMessagesLabel": "Nouveaux messages", + "localeRU": "Russe", "copyServerKeys": "Copier les clés", "verfiyResumeButton": "Vérifier\/reprendre", "fileCheckingStatus": "Vérification de l'état du téléchargement", @@ -55,7 +65,6 @@ "peerOfflineMessage": "Le contact est hors ligne, les messages ne peuvent pas être transmis pour le moment.", "blockBtn": "Bloquer le contact", "savePeerHistory": "Enregistrer l'historique", - "savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au contact.", "dontSavePeerHistory": "Supprimer l'historique", "unblockBtn": "Débloquer le contact", "blockUnknownLabel": "Bloquer les pairs inconnus", @@ -190,7 +199,6 @@ "radioNoPassword": "Non chiffré (pas de mot de passe)", "radioUsePassword": "Mot de passe", "copiedToClipboardNotification": "Copié dans le presse-papier", - "copyBtn": "Copier", "editProfile": "Modifier le profil", "newProfile": "Nouveau profil", "defaultProfileName": "Alice", @@ -210,6 +218,7 @@ "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe", "newGroupBtn": "Créer un nouveau groupe", "copiedClipboardNotification": "Copié dans le presse-papier", + "copyBtn": "Copier", "pendingLabel": "En attente", "acknowledgedLabel": "Accusé de réception", "couldNotSendMsgError": "Impossible d'envoyer ce message", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index fce93d69..113411cd 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,52 +1,62 @@ { "@@locale": "it", - "@@last_modified": "2021-11-11T01:02:08+01:00", - "newMessagesLabel": "New Messages", - "localeRU": "Russian", - "copyServerKeys": "Copy keys", - "verfiyResumeButton": "Verify\/resume", - "fileCheckingStatus": "Checking download status", - "fileInterrupted": "Interrupted", - "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.", - "deleteServerConfirmBtn": "Really delete server", - "deleteServerSuccess": "Successfully deleted server", - "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", - "copyAddress": "Copy Address", - "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!", - "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", - "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", - "serverDescriptionLabel": "Server Description", - "serverAddress": "Server Address", - "editServerTitle": "Edit Server", - "addServerTitle": "Add Server", - "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", + "@@last_modified": "2021-11-21T17:42:07+01:00", + "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": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.", + "newMessagesLabel": "Nuovi messaggi", + "localeRU": "Russo", + "copyServerKeys": "Copia chiavi", + "verfiyResumeButton": "Verifica\/riprendi", + "fileCheckingStatus": "Controllo dello stato del download", + "fileInterrupted": "Interrotto", + "fileSavedTo": "Salvato in", + "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.", + "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.", + "deleteServerConfirmBtn": "Elimina davvero il server", + "deleteServerSuccess": "Server eliminato con successo", + "enterCurrentPasswordForDeleteServer": "Inserisci la password attuale per eliminare questo server", + "copyAddress": "Copia indirizzo", + "settingServersDescription": "L'esperimento dei server di hosting permette di allocare e gestire i server Cwtch", + "settingServers": "Server di hosting", + "enterServerPassword": "Inserisci la password per sbloccare il server", + "unlockProfileTip": "Crea o sblocca un profilo per iniziare!", + "unlockServerTip": "Crea o sblocca un server per iniziare!", + "addServerTooltip": "Aggiungi nuovo server", + "serversManagerTitleShort": "Gestisci i server", + "serversManagerTitleLong": "Server che gestisci", + "saveServerButton": "Salva il server", + "serverAutostartDescription": "Controlla se l'applicazione avvierà automaticamente il server all'avvio", + "serverAutostartLabel": "Avvio automatico", + "serverEnabledDescription": "Avvia o arresta il server", + "serverEnabled": "Server abilitato", + "serverDescriptionDescription": "La tua descrizione del server solo per gestione personale, non sarà mai condivisa", + "serverDescriptionLabel": "Descrizione del server", + "serverAddress": "Indirizzo del server", + "editServerTitle": "Modifica il server", + "addServerTitle": "Aggiungi server", + "titleManageProfilesShort": "Profili", + "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", "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.", "settingFileSharing": "Condivisione file", "tooltipSendFile": "Invia file", "messageFileOffered": "Il contatto offre l'invio di un file", - "messageFileSent": "You sent a file", - "messageEnableFileSharing": "Enable the file sharing experiment to view this message.", - "labelFilesize": "Size", - "labelFilename": "Filename", - "downloadFileButton": "Download", - "openFolderButton": "Open Folder", - "retrievingManifestMessage": "Retrieving file information...", - "streamerModeLabel": "Streamer\/Presentation Mode", - "archiveConversation": "Archive this Conversation", + "messageFileSent": "Hai inviato un file", + "messageEnableFileSharing": "Abilita l'esperimento di condivisione dei file per visualizzare questo messaggio.", + "labelFilesize": "Dimensione", + "labelFilename": "Nome del file", + "downloadFileButton": "Scarica", + "openFolderButton": "Apri cartella", + "retrievingManifestMessage": "Recupero delle informazioni sul file in corso...", + "streamerModeLabel": "Modalità Streamer\/Presentazione", + "archiveConversation": "Archivia questa conversazione", "profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi", "addPeerTab": "Aggiungi un peer", "addPeer": "Aggiungi peer", @@ -55,29 +65,28 @@ "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", + "blockUnknownConnectionsEnabledDescription": "Le connessioni da contatti sconosciuti sono bloccate. Puoi modificare questa impostazione in Impostazioni", "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", + "showMessageButton": "Mostra il messaggio", + "blockedMessageMessage": "Questo messaggio proviene da un profilo che hai bloccato.", + "placeholderEnterMessage": "Scrivi un messaggio...", + "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.", + "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.", + "addContactConfirm": "Aggiungi %1 come contatto", + "addContact": "Aggiungi contatto", + "contactGoto": "Vai alla conversazione con %1", + "settingUIColumnOptionSame": "Stessa impostazione della modalità verticale", + "settingUIColumnDouble14Ratio": "Doppia (1:4)", + "settingUIColumnDouble12Ratio": "Doppia (1:2)", + "settingUIColumnSingle": "Singola", + "settingUIColumnLandscape": "Colonne dell'interfaccia utente in modalità orizzontale", + "settingUIColumnPortrait": "Colonne dell'interfaccia utente in modalità verticale", + "localePl": "Polacco", + "tooltipRemoveThisQuotedMessage": "Rimuovi il messaggio citato.", + "tooltipReplyToThisMessage": "Rispondi a questo messaggio", "tooltipRejectContactRequest": "Rifiuta questa richiesta di contatto", "tooltipAcceptContactRequest": "Accetta questa richiesta di contatto.", "notificationNewMessageFromGroup": "Nuovo messaggio in un gruppo!", @@ -190,7 +199,6 @@ "radioNoPassword": "Non criptato (senza password)", "radioUsePassword": "Password", "copiedToClipboardNotification": "Copiato negli Appunti", - "copyBtn": "Copia", "editProfile": "Modifica profilo", "newProfile": "Nuovo profilo", "defaultProfileName": "Alice", @@ -210,6 +218,7 @@ "acceptGroupInviteLabel": "Vuoi accettare l'invito a", "newGroupBtn": "Crea un nuovo gruppo", "copiedClipboardNotification": "Copiato negli Appunti", + "copyBtn": "Copia", "pendingLabel": "In corso", "acknowledgedLabel": "Riconosciuto", "couldNotSendMsgError": "Impossibile inviare questo messaggio", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index aa2e5366..a8fd1a3e 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,8 +1,18 @@ { "@@locale": "pl", - "@@last_modified": "2021-11-11T01:02:08+01:00", - "newMessagesLabel": "New Messages", - "localeRU": "Russian", + "@@last_modified": "2021-11-21T17:42:07+01:00", + "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": "Nowe wiadomości", + "localeRU": "Rosyjski", "copyServerKeys": "Kopiuj klucze", "verfiyResumeButton": "Zweryfikuj\/wznów", "fileCheckingStatus": "Sprawdzanie stanu pobierania", @@ -12,26 +22,26 @@ "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.", "deleteServerConfirmBtn": "Naprawdę usuń serwer", "deleteServerSuccess": "Pomyślnie usunięto serwer", - "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", + "enterCurrentPasswordForDeleteServer": "Wprowadź aktualne hasło, aby usunąć ten serwer", "copyAddress": "Skopiuj adres", "settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers", - "settingServers": "Hosting Servers", - "enterServerPassword": "Enter password to unlock server", + "settingServers": "Hosting serwerów", + "enterServerPassword": "Wprowadź hasło, aby odblokować serwer", "unlockProfileTip": "Please create or unlock a profile to begin!", "unlockServerTip": "Please create or unlock a server to begin!", - "addServerTooltip": "Add new server", - "serversManagerTitleShort": "Servers", - "serversManagerTitleLong": "Servers You Host", - "saveServerButton": "Save Server", + "addServerTooltip": "Dodaj nowy serwer", + "serversManagerTitleShort": "Serwery", + "serversManagerTitleLong": "Serwery, które hostujesz", + "saveServerButton": "Zapisz serwer", "serverAutostartDescription": "Controls if the application will automatically launch the server on start", "serverAutostartLabel": "Autostart", - "serverEnabledDescription": "Start or stop the server", - "serverEnabled": "Server Enabled", + "serverEnabledDescription": "Uruchom lub zatrzymaj serwer", + "serverEnabled": "Serwer włączony", "serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared", - "serverDescriptionLabel": "Server Description", - "serverAddress": "Server Address", - "editServerTitle": "Edit Server", - "addServerTitle": "Add Server", + "serverDescriptionLabel": "Opis serwera", + "serverAddress": "Adres serwera", + "editServerTitle": "Edytuj serwer", + "addServerTitle": "Dodaj serwer", "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", "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ć.", @@ -55,70 +65,69 @@ "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...", + "showMessageButton": "Pokaż wiadomość", + "blockedMessageMessage": "Ta wiadomość pochodzi z profilu, który został przez Ciebie zablokowany.", + "placeholderEnterMessage": "Wpisz wiadomość...", "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", + "addContactConfirm": "Dodaj kontakt %1", + "addContact": "Dodaj kontakt", + "contactGoto": "Przejdź do rozmowy z %1", + "settingUIColumnOptionSame": "Tak samo jak w przypadku trybu portretowego", + "settingUIColumnDouble14Ratio": "Podwójny (1:4)", + "settingUIColumnDouble12Ratio": "Podwójny (1:2)", + "settingUIColumnSingle": "Pojedynczy", "settingUIColumnLandscape": "UI Columns in Landscape Mode", "settingUIColumnPortrait": "UI Columns in Portrait Mode", - "localePl": "Polish", - "tooltipRemoveThisQuotedMessage": "Remove quoted message.", - "tooltipReplyToThisMessage": "Reply to this message", - "tooltipRejectContactRequest": "Reject this contact request", - "tooltipAcceptContactRequest": "Accept this contact request.", - "notificationNewMessageFromGroup": "New message in a group!", - "notificationNewMessageFromPeer": "New message from a contact!", - "tooltipHidePassword": "Hide Password", - "tooltipShowPassword": "Show Password", + "localePl": "Polski", + "tooltipRemoveThisQuotedMessage": "Usuń cytowaną wiadomość.", + "tooltipReplyToThisMessage": "Odpowiedz na tę wiadomość", + "tooltipRejectContactRequest": "Odrzuć tę prośbę o kontakt", + "tooltipAcceptContactRequest": "Zaakceptuj tę prośbę o kontakt.", + "notificationNewMessageFromGroup": "Nowa wiadomość w grupie!", + "notificationNewMessageFromPeer": "Nowa wiadomość od kontaktu!", + "tooltipHidePassword": "Ukryj hasło", + "tooltipShowPassword": "Pokaż hasło", "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.", - "shutdownCwtchAction": "Shutdown Cwtch", + "shutdownCwtchAction": "Zamknij Cwtch", "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", + "shutdownCwtchDialogTitle": "Zamknąć Cwtch?", + "shutdownCwtchTooltip": "Zamknij Cwtch", + "malformedMessage": "Źle sformatowana wiadomość", + "profileDeleteSuccess": "Pomyślnie usunięto profil", + "debugLog": "Włącz logowanie debugowania konsoli", + "torNetworkStatus": "Stan sieci Tor", "addContactFirst": "Add or pick a contact to begin chatting.", "createProfileToBegin": "Please create or unlock a profile to begin", - "nickChangeSuccess": "Profile nickname changed successfully", + "nickChangeSuccess": "Nick w profilu został zmieniony pomyślnie", "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", + "deleteProfileSuccess": "Pomyślnie usunięto profil", + "sendInvite": "Wyślij kontakt lub zaproszenie do grupy", + "sendMessage": "Wyślij wiadomość", + "cancel": "Anuluj", "resetTor": "Reset", - "torStatus": "Tor Status", - "torVersion": "Tor Version", + "torStatus": "Status Tor", + "torVersion": "Wersja Tor", "sendAnInvitation": "You sent an invitation for: ", "contactSuggestion": "This is a contact suggestion for: ", - "rejected": "Rejected!", - "accepted": "Accepted!", + "rejected": "Odrzucone!", + "accepted": "Przyjęte!", "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", + "newPassword": "Nowe hasło", + "yesLeave": "Tak, wyjdź z tej rozmowy", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", - "leaveGroup": "Leave This Conversation", + "leaveGroup": "Wyjdź z tej rozmowy", "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", + "titleManageServers": "Zarządzaj serwerami", "dateNever": "Never", "dateLastYear": "Last Year", "dateYesterday": "Yesterday", @@ -190,7 +199,6 @@ "radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "Password", "copiedToClipboardNotification": "Copied to Clipboard", - "copyBtn": "Copy", "editProfile": "Edit Profille", "newProfile": "New Profile", "defaultProfileName": "Alice", @@ -210,6 +218,7 @@ "acceptGroupInviteLabel": "Do you want to accept the invitation to", "newGroupBtn": "Create new group", "copiedClipboardNotification": "Copied to clipboard", + "copyBtn": "Copy", "pendingLabel": "Pending", "acknowledgedLabel": "Acknowledged", "couldNotSendMsgError": "Could not send this message", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 137e8a2b..533631d0 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,16 @@ { "@@locale": "pt", - "@@last_modified": "2021-11-11T01:02:08+01:00", + "@@last_modified": "2021-11-21T17:42:07+01:00", + "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", "localeRU": "Russian", "copyServerKeys": "Copy keys", @@ -55,7 +65,6 @@ "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", @@ -190,7 +199,6 @@ "radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "Password", "copiedToClipboardNotification": "Copiado", - "copyBtn": "Copiar", "editProfile": "Edit Profille", "newProfile": "New Profile", "defaultProfileName": "Alice", @@ -210,6 +218,7 @@ "acceptGroupInviteLabel": "Você quer aceitar o convite para", "newGroupBtn": "Criar novo grupo", "copiedClipboardNotification": "Copiado", + "copyBtn": "Copiar", "pendingLabel": "Pendente", "acknowledgedLabel": "Confirmada", "couldNotSendMsgError": "Não deu para enviar esta mensagem", diff --git a/lib/model.dart b/lib/model.dart index d2f68bda..9435dc3f 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -142,14 +142,10 @@ class ContactListState extends ChangeNotifier { } void addAll(Iterable newContacts) { - print("****** contactListState.addAll()... *********"); _contacts.addAll(newContacts); servers?.clearGroups(); - print("contact len: ${_contacts.length}"); _contacts.forEach((contact) { - //print("looking at contact ${contact.onion} (${contact.isGroup})..."); if (contact.isGroup) { - print("contactList adding group ${contact.onion} to ${contact.server}"); servers?.addGroup(contact); } }); @@ -159,7 +155,6 @@ class ContactListState extends ChangeNotifier { void add(ContactInfoState newContact) { _contacts.add(newContact); if (newContact.isGroup) { - print("contactList adding group ${newContact.onion} to ${newContact.server}"); servers?.addGroup(newContact); } notifyListeners(); @@ -299,6 +294,13 @@ class ProfileInfoState extends ChangeNotifier { // TODO Keys... return RemoteServerInfoState(onion: server["onion"], description: server["description"], status: server["status"]); })); + + this._contacts.contacts.forEach((contact) { + if (contact.isGroup) { + _servers.addGroup(contact); + } + }); + notifyListeners(); } } diff --git a/lib/models/profileservers.dart b/lib/models/profileservers.dart index ebd1cf99..cdf103f3 100644 --- a/lib/models/profileservers.dart +++ b/lib/models/profileservers.dart @@ -19,7 +19,7 @@ class ProfileServerListState extends ChangeNotifier { void updateServerState(String onion, String status) { int idx = _servers.indexWhere((element) => element.onion == onion); if (idx >= 0) { - _servers[idx] = RemoteServerInfoState(onion: onion, description: _servers[idx].description, status: status); + _servers[idx].status = status; } else { print("Tried to update server cache without a starting state...this is probably an error"); } @@ -31,11 +31,21 @@ class ProfileServerListState extends ChangeNotifier { _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; }); } @@ -45,8 +55,6 @@ class ProfileServerListState extends ChangeNotifier { } void addGroup(ContactInfoState group) { - print("serverList adding group ${group.onion} to ${group.server}"); - int idx = _servers.indexWhere((element) => element.onion == group.server); if (idx >= 0) { _servers[idx].addGroup(group); @@ -59,7 +67,7 @@ class ProfileServerListState extends ChangeNotifier { class RemoteServerInfoState extends ChangeNotifier { final String onion; - final String status; + String status; String description; List _groups = []; @@ -71,21 +79,14 @@ class RemoteServerInfoState extends ChangeNotifier { } void clearGroups() { - print("Server CLEARING group"); - description = "cleared groups"; _groups = []; } void addGroup(ContactInfoState group) { - print("server $onion adding group ${group.onion}"); _groups.add(group); - print("now has ${_groups.length}"); - description = "i have ${_groups.length} groups"; notifyListeners(); } - int get groupsLen => _groups.length; - List get groups => _groups.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier } diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 2355fd57..03084ca2 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -107,8 +107,7 @@ class _AddEditProfileViewState extends State { labelText: AppLocalizations.of(context)!.yourDisplayName, validator: (value) { if (value.isEmpty) { - // TODO l10n ize - return "Please enter a display name"; + return AppLocalizations.of(context)!.displayNameTooltip; } return null; }, diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index af776607..a05ef4df 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -113,11 +113,11 @@ class _ContactsViewState extends State { Clipboard.setData(new ClipboardData(text: Provider.of(context, listen: false).onion)); })); - // Manage servers + // Manage known Servers if (Provider.of(context, listen: false).isExperimentEnabled(ServerManagementExperiment)) { actions.add(IconButton( icon: Icon(CwtchIcons.dns_24px), - tooltip: "Manage known servers", //AppLocalizations.of(context)!.copyAddress, + tooltip: AppLocalizations.of(context)!.manageKnownServersButton, onPressed: () { _pushServers(); })); @@ -176,7 +176,7 @@ class _ContactsViewState extends State { Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return MultiProvider( - providers: [ChangeNotifierProvider(create: (context) => profile.serverList), Provider.value(value: Provider.of(context))], + providers: [ChangeNotifierProvider(create: (context) => profile), Provider.value(value: Provider.of(context))], child: ProfileServersView(), ); }, diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart index aad94f85..7822ff96 100644 --- a/lib/views/profileserversview.dart +++ b/lib/views/profileserversview.dart @@ -5,7 +5,10 @@ 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 { @@ -23,16 +26,31 @@ class _ProfileServersView extends State { @override Widget build(BuildContext context) { + var knownServers = Provider.of(context).serverList.servers.map((RemoteServerInfoState remoteServer) { return remoteServer.onion + ".onion"; }).toSet(); + var importServerList = Provider.of(context).servers.where((server) => !knownServers.contains(server.onion) ).map>((ServerInfoState serverInfo) { + return DropdownMenuItem( + value: serverInfo.onion, + child: Text( + serverInfo.description.isNotEmpty ? serverInfo.description : serverInfo.onion, + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(); + + importServerList.insert(0, DropdownMenuItem( + value: "", + child: Text(AppLocalizations.of(context)!.importLocalServerSelectText))); + return Scaffold( appBar: AppBar( title: Text(MediaQuery .of(context) .size - .width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), + .width > 600 ? AppLocalizations.of(context)!.manageKnownServersLong : AppLocalizations.of(context)!.manageKnownServersShort), //actions: getActions(), ), - body: Consumer( - builder: (context, servers, child) { + body: Consumer(builder: (context, profile, child) { + ProfileServerListState servers = profile.serverList; final tiles = servers.servers.map((RemoteServerInfoState server) { return ChangeNotifierProvider.value( value: server, @@ -46,14 +64,84 @@ class _ProfileServersView extends State { tiles: tiles, ).toList(); - // TODO: add import row from global servers - divided.insert(0, Row( children: [Text("Import server from global list if any")])); + final importCard = Card( child: ListTile( + title: Text(AppLocalizations.of(context)!.importLocalServerLabel), + leading: Icon(CwtchIcons.add_circle_24px , color: Provider.of(context).current().mainTextColor()), + trailing: DropdownButton( + onChanged: (String? importServer) { + if (importServer!.isNotEmpty) { + var server = Provider.of(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(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(context); + profile.serverList.getServer(serverHandle)?.updateDescription(serverDesc); + Provider.of(context, listen: false).cwtch.SetContactAttribute(profile.onion, serverHandle, "local.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; + }, + ); + } + } \ No newline at end of file diff --git a/lib/views/remoteserverview.dart b/lib/views/remoteserverview.dart index 22bee1bf..e194da05 100644 --- a/lib/views/remoteserverview.dart +++ b/lib/views/remoteserverview.dart @@ -48,118 +48,102 @@ class _RemoteServerViewState extends State { @override Widget build(BuildContext context) { - return Consumer2(builder: (context, serverInfoState, settings, child) { + return Consumer3(builder: (context, profile, serverInfoState, settings, child) { return Scaffold( appBar: AppBar( title: Text(ctrlrDesc.text.isNotEmpty ? ctrlrDesc.text : serverInfoState.onion) ), - body: LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { - return Scrollbar( - isAlwaysShown: true, - child: SingleChildScrollView( - clipBehavior: Clip.antiAlias, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Form( - key: _formKey, - child: Container( - margin: EdgeInsets.fromLTRB(30, 0, 30, 10), - padding: EdgeInsets.fromLTRB(20, 0, 20, 10), - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + 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 + ), - 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(context, listen: false).cwtch.SetContactAttribute(profile.onion, serverInfoState.onion, "local.server.description", ctrlrDesc.text); + serverInfoState.updateDescription(ctrlrDesc.text); + }, + ), - // Description - Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: 20, - ), - CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel), - Text(AppLocalizations.of(context)!.serverDescriptionDescription), - SizedBox( - height: 20, - ), - CwtchButtonTextField( - controller: ctrlrDesc, - readonly: false, - tooltip: "Save", //TODO localize - labelText: "Description", // TODO localize - icon: Icon(Icons.save), - onPressed: () { - // TODO save - }, - ) - ]), + SizedBox( + height: 20, + ), - Text("Groups on this server"), - _buildGroupsList(serverInfoState), + Padding(padding: EdgeInsets.all(8), child: Text( AppLocalizations.of(context)!.groupsOnThisServerLabel),), + Expanded(child: _buildGroupsList(serverInfoState)) + ]))); - ])))))); - }),); - }); + }); } Widget _buildGroupsList(RemoteServerInfoState serverInfoState) { - print("groups: ${serverInfoState.groups} lenMethod: ${serverInfoState.groupsLen} len: ${serverInfoState.groups.length}"); final tiles = serverInfoState.groups.map((ContactInfoState group) { - print("building group tile for ${group.onion}"); - return ChangeNotifierProvider.value(key: ValueKey(group.profileOnion + "" + group.onion), value: group, builder: (_, __) => RepaintBoundary(child: _buildGroupRow(group))); - }); + return ChangeNotifierProvider.value( + value: group, + builder: (context, child) => RepaintBoundary(child: _buildGroupRow(group)), // ServerRow()), + ); + }, + ); + final divided = ListTile.divideTiles( context: context, tiles: tiles, ).toList(); - return RepaintBoundary(child: ListView(children: divided)); - } - void _savePressed() { + var size = MediaQuery.of(context).size; - var server = Provider.of(context, listen: false); + int cols = ((size.width - 50) / 500).ceil(); + final double itemHeight = 60; // magic arbitary + final double itemWidth = (size.width - 50 /* magic padding guess */) / cols; - Provider.of(context, listen: false) - .cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text); - server.setDescription(ctrlrDesc.text); - - - if (_formKey.currentState!.validate()) { - // TODO support change password - } - Navigator.of(context).pop(); + return GridView.count(crossAxisCount: cols, childAspectRatio: (itemWidth / itemHeight), children: divided); } Widget _buildGroupRow(ContactInfoState group) { - return Column( - children: [ - Text( - group.nickname, - style: Provider.of(context).biggerFont.apply(color: Provider.of(context).theme.portraitOnlineBorderColor()), - softWrap: true, - overflow: TextOverflow.ellipsis, - ), - Visibility( - visible: !Provider.of(context).streamerMode, - child: ExcludeSemantics( - child: Text( - group.onion, - softWrap: true, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: Provider.of(context).theme.portraitOnlineBorderColor()), - ))) - ], + return Padding( + padding: const EdgeInsets.all(6.0), //border size + child: Column( + children: [ + Text( + group.nickname, + style: Provider.of(context).biggerFont.apply(color: Provider.of(context).theme.portraitOnlineBorderColor()), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + Visibility( + visible: !Provider.of(context).streamerMode, + child: ExcludeSemantics( + child: Text( + group.onion, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Provider.of(context).theme.portraitOnlineBorderColor()), + ))) + ]) ); } diff --git a/lib/widgets/remoteserverrow.dart b/lib/widgets/remoteserverrow.dart index 0df655d2..339a170d 100644 --- a/lib/widgets/remoteserverrow.dart +++ b/lib/widgets/remoteserverrow.dart @@ -25,53 +25,54 @@ class _RemoteServerRowState extends State { var server = Provider.of(context); var description = server.description.isNotEmpty ? server.description : server.onion; var running = server.status == "Synced"; - 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(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), - size: 64) + return Consumer( + 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(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), + size: 64) - ), - Expanded( - child: Column( - children: [ - Text( - description, - semanticsLabel: description, - style: Provider.of(context).biggerFont.apply(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), - softWrap: true, - overflow: TextOverflow.ellipsis, - ), - Visibility( - visible: !Provider.of(context).streamerMode, - child: ExcludeSemantics( - child: Text( - server.onion, - softWrap: true, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), - ))) - ], - )), + ), + Expanded( + child: Column( + children: [ + Text( + description, + semanticsLabel: description, + style: Provider.of(context).biggerFont.apply(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + Visibility( + visible: !Provider.of(context).streamerMode, + child: ExcludeSemantics( + child: Text( + server.onion, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + ))) + ], + )), - ]), - onTap: () { - Navigator.of(context).push(MaterialPageRoute( + ]), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( settings: RouteSettings(name: "remoteserverview"), builder: (BuildContext context) { return MultiProvider( - providers: [ChangeNotifierProvider(create: (context) => server), Provider.value(value: Provider.of(context))], + providers: [Provider.value(value: profile), ChangeNotifierProvider(create: (context) => server), Provider.value(value: Provider.of(context))], child: RemoteServerView(), ); })); - } - )); - } - -} \ No newline at end of file + } + ));}); + } +} From 6180b881724c04539905740bb24fe78aafa3523f Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Fri, 10 Dec 2021 15:37:08 -0800 Subject: [PATCH 25/28] port to new cwtch storage API --- lib/model.dart | 2 +- lib/models/profileservers.dart | 3 ++- lib/views/profileserversview.dart | 13 +++++++++++-- lib/views/remoteserverview.dart | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/model.dart b/lib/model.dart index 9435dc3f..82246ee0 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -292,7 +292,7 @@ class ProfileInfoState extends ChangeNotifier { List servers = jsonDecode(serversJson); this._servers.replace(servers.map((server) { // TODO Keys... - return RemoteServerInfoState(onion: server["onion"], description: server["description"], status: server["status"]); + return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]); })); this._contacts.contacts.forEach((contact) { diff --git a/lib/models/profileservers.dart b/lib/models/profileservers.dart index cdf103f3..5f422538 100644 --- a/lib/models/profileservers.dart +++ b/lib/models/profileservers.dart @@ -67,11 +67,12 @@ class ProfileServerListState extends ChangeNotifier { class RemoteServerInfoState extends ChangeNotifier { final String onion; + final int identifier; String status; String description; List _groups = []; - RemoteServerInfoState({required this.onion, required this.description, required this.status}); + RemoteServerInfoState({required this.onion, required this.identifier, required this.description, required this.status}); void updateDescription(String newDescription) { this.description = newDescription; diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart index 7822ff96..37337c54 100644 --- a/lib/views/profileserversview.dart +++ b/lib/views/profileserversview.dart @@ -118,8 +118,17 @@ class _ProfileServersView extends State { // 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(context); - profile.serverList.getServer(serverHandle)?.updateDescription(serverDesc); - Provider.of(context, listen: false).cwtch.SetContactAttribute(profile.onion, serverHandle, "local.server.description", serverDesc); + if (profile.serverList.getServer(serverHandle) != null) { + profile.serverList.getServer(serverHandle)?.updateDescription( + serverDesc); + + Provider + .of(context, listen: false) + .cwtch + .SetConversationAttribute(profile.onion, profile.serverList + .getServer(serverHandle) + !.identifier, "server.description", serverDesc); + } }); Navigator.of(context).pop(); }); diff --git a/lib/views/remoteserverview.dart b/lib/views/remoteserverview.dart index e194da05..bc940807 100644 --- a/lib/views/remoteserverview.dart +++ b/lib/views/remoteserverview.dart @@ -84,7 +84,7 @@ class _RemoteServerViewState extends State { labelText: AppLocalizations.of(context)!.fieldDescriptionLabel, icon: Icon(Icons.save), onPressed: () { - Provider.of(context, listen: false).cwtch.SetContactAttribute(profile.onion, serverInfoState.onion, "local.server.description", ctrlrDesc.text); + Provider.of(context, listen: false).cwtch.SetConversationAttribute(profile.onion, serverInfoState.identifier, "server.description", ctrlrDesc.text); serverInfoState.updateDescription(ctrlrDesc.text); }, ), From 2257a63e179095938aacec2ab0d39b0d7ef8ad53 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Fri, 10 Dec 2021 23:14:42 -0800 Subject: [PATCH 26/28] new libcwtch --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index 3c6d2eca..dcda997e 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2021-12-08-00-32-v1.5.0-7-g28a13aa \ No newline at end of file +2021-12-11-02-00-v1.5.0-9-gaa102bd \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 33bf0526..8f72ef13 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2021-12-08-05-32-v1.5.0-7-g28a13aa \ No newline at end of file +2021-12-11-07-00-v1.5.0-9-gaa102bd \ No newline at end of file From 63ed835880eec1024651374442066b397613f98d Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Fri, 10 Dec 2021 23:26:47 -0800 Subject: [PATCH 27/28] remove commented out code --- lib/views/profileserversview.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart index 37337c54..7ac06520 100644 --- a/lib/views/profileserversview.dart +++ b/lib/views/profileserversview.dart @@ -47,7 +47,6 @@ class _ProfileServersView extends State { .of(context) .size .width > 600 ? AppLocalizations.of(context)!.manageKnownServersLong : AppLocalizations.of(context)!.manageKnownServersShort), - //actions: getActions(), ), body: Consumer(builder: (context, profile, child) { ProfileServerListState servers = profile.serverList; From 816425cd4d513b5898bebea8e7789c8d9a675567 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 13 Dec 2021 15:42:42 -0800 Subject: [PATCH 28/28] Fix #255 - clicking Open now causes popup to close. Also importing new translatable strings + a few nicer URL options --- lib/l10n/intl_de.arb | 217 +++++++------- lib/l10n/intl_en.arb | 7 +- lib/l10n/intl_es.arb | 265 +++++++++-------- lib/l10n/intl_fr.arb | 351 +++++++++++----------- lib/l10n/intl_it.arb | 477 +++++++++++++++--------------- lib/l10n/intl_pl.arb | 225 +++++++------- lib/l10n/intl_pt.arb | 53 ++-- lib/l10n/intl_ru.arb | 81 ++--- lib/views/globalsettingsview.dart | 4 +- lib/widgets/messagebubble.dart | 3 +- 10 files changed, 867 insertions(+), 816 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index b08f6de8..71936b39 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,11 @@ { "@@locale": "de", - "@@last_modified": "2021-11-21T17:42:07+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", @@ -10,7 +15,6 @@ "importLocalServerButton": "Import %1", "importLocalServerSelectText": "Select Local Server", "importLocalServerLabel": "Import a locally hosted server", - "savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.", "newMessagesLabel": "New Messages", "localeRU": "Russian", "copyServerKeys": "Copy keys", @@ -18,8 +22,8 @@ "fileCheckingStatus": "Checking download status", "fileInterrupted": "Interrupted", "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.", + "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", "deleteServerSuccess": "Successfully deleted server", "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", @@ -42,8 +46,8 @@ "serverAddress": "Server Address", "editServerTitle": "Edit Server", "addServerTitle": "Add Server", + "downloadFileButton": "Herunterladen", "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.", "settingFileSharing": "File Sharing", "tooltipSendFile": "Send File", @@ -52,24 +56,12 @@ "messageEnableFileSharing": "Enable the file sharing experiment to view this message.", "labelFilesize": "Size", "labelFilename": "Filename", - "downloadFileButton": "Herunterladen", "openFolderButton": "Open Folder", "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", "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", - "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", - "networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...", "showMessageButton": "Show Message", "blockedMessageMessage": "This message is from a profile you have blocked.", "placeholderEnterMessage": "Type a message...", @@ -89,17 +81,31 @@ "tooltipReplyToThisMessage": "Reply to this message", "tooltipRejectContactRequest": "Reject 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!", "notificationNewMessageFromPeer": "Neue Nachricht von einem Kontakt!", "tooltipHidePassword": "Password verstecken", "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.", "shutdownCwtchAction": "Cwtch schliessen", "shutdownCwtchDialog": "Bist du sicher, dass du Cwtch schliessen möchtest? Alle Verbindungen werden geschlossen und die App wird beendet.", "shutdownCwtchDialogTitle": "Cwtch schliessen?", "shutdownCwtchTooltip": "Cwtch schliessen", - "malformedMessage": "Fehlerhafte Nachricht", "profileDeleteSuccess": "Profil erfolgreich gelöscht", "debugLog": "Konsolendebuglogging aktivivieren", "torNetworkStatus": "Tor Netzwerkstatus", @@ -115,7 +121,6 @@ "torStatus": "Tor Status", "torVersion": "Tor Version", "sendAnInvitation": "Du hast eine Einladung geschickt für: ", - "contactSuggestion": "Dieser Kontaktvorschlag ist für: ", "rejected": "Abgelehnt!", "accepted": "Angenommen!", "chatHistoryDefault": "Diese Unterhaltung wird gelöscht sobald Cwtch geschlossen wird! Der Nachrichtenverlauf für jede Unterhaltung kann im Einstellungsmenü oben rechts geändert werden.", @@ -124,9 +129,6 @@ "reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Unterhaltung beenden möchtest? Alle Nachrichten und Attribute werden gelöscht.", "leaveGroup": "Unterhaltung beenden", "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", "dateNever": "Nie", "dateLastYear": "Letzes Jahr", @@ -134,80 +136,110 @@ "dateLastMonth": "Letzter Monat", "dateRightNow": "Jetzt", "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", "tooltipUnlockProfiles": "Entsperre verschlüsselte Profile durch Eingabe des Passworts.", + "titleManageContacts": "Unterhaltungen", + "tooltipAddContact": "Neuen Kontakt oder Unterhaltung hinzufügen", "tooltipOpenSettings": "Öfffne das Einstellungsmenü", - "invalidImportString": "Ungültiger Importstring", "contactAlreadyExists": "Kontakt existiert bereits", + "invalidImportString": "Ungültiger Importstring", "conversationSettings": "Unterhaltungseinstellungen", "enterCurrentPasswordForDelete": "Bitte gib das aktuelle Passwort ein, um diese Profil zu löschen.", "enableGroups": "Gruppenchat aktivieren", - "experimentsEnabled": "Experimente aktiviert", "localeIt": "Italiana", "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", "localeFr": "Frances", "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", - "password2Label": "Passwort erneut eingeben", - "password1Label": "Passwort", "currentPasswordLabel": "aktuelles Passwort", "yourDisplayName": "Dein Anzeigename", - "noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.", - "radioNoPassword": "Unverschlüsselt (kein Passwort)", - "radioUsePassword": "Passwort", - "copiedToClipboardNotification": "in die Zwischenablage kopiert", - "editProfile": "Profil bearbeiten", - "newProfile": "Neues Profil", - "defaultProfileName": "Alice", - "profileName": "Anzeigename", - "editProfileTitle": "Profil bearbeiten", + "unblockBtn": "Anderen Nutzer entsperren", + "dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen", + "savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.", + "blockBtn": "Anderen Nutzer blockieren", + "displayNameLabel": "Angezeigename", + "peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden", + "peerBlockedMessage": "Anderer Nutzer ist blockiert", + "dmTooltip": "Klicken, um Direktnachricht zu senden", + "peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.", + "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", + "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", "saveBtn": "Speichern", - "displayNameLabel": "Angezeigename", "addressLabel": "Adresse", "puzzleGameBtn": "Puzzlespiel", "bulletinsBtn": "Meldungen", @@ -222,42 +254,15 @@ "pendingLabel": "Bestätigung ausstehend", "acknowledgedLabel": "bestätigt", "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.", - "addListItemBtn": "Element hinzufügen", - "searchList": "Liste durchsuchen", - "update": "Update", "inviteBtn": "Einladen", "inviteToGroupLabel": "In die Gruppe einladen", "groupNameLabel": "Gruppenname", - "viewServerInfo": "Serverinfo", - "serverSynced": "synchronisiert", - "serverConnectivityDisconnected": "Server getrennt", - "serverConnectivityConnected": "Server verbunden", - "serverInfo": "Server-Informationen", "invitationLabel": "Einladung", "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...", "postNewBulletinLabel": "Neue Meldung veröffentlichen", "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", "defaultGroupName": "Tolle Gruppe", "createGroupTitle": "Gruppe Anlegen" diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4069f313..f1546402 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,11 @@ { "@@locale": "en", - "@@last_modified": "2021-11-21T17:42:07+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", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index bb62828f..ce52d391 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,11 @@ { "@@locale": "es", - "@@last_modified": "2021-11-21T17:42:07+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", @@ -10,7 +15,6 @@ "importLocalServerButton": "Import %1", "importLocalServerSelectText": "Select Local Server", "importLocalServerLabel": "Import a locally hosted server", - "savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.", "newMessagesLabel": "New Messages", "localeRU": "Russian", "copyServerKeys": "Copy keys", @@ -18,8 +22,8 @@ "fileCheckingStatus": "Checking download status", "fileInterrupted": "Interrupted", "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.", + "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", "deleteServerSuccess": "Successfully deleted server", "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", @@ -43,7 +47,6 @@ "editServerTitle": "Edit Server", "addServerTitle": "Add Server", "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.", "settingFileSharing": "File Sharing", "tooltipSendFile": "Send File", @@ -55,21 +58,10 @@ "downloadFileButton": "Download", "openFolderButton": "Open Folder", "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", "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", - "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", - "networkStatusConnecting": "Conectando a la red y a los contactos...", "showMessageButton": "Show Message", "blockedMessageMessage": "This message is from a profile you have blocked.", "placeholderEnterMessage": "Type a message...", @@ -93,7 +85,6 @@ "notificationNewMessageFromPeer": "New message from a contact!", "tooltipHidePassword": "Hide 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.", "shutdownCwtchAction": "Shutdown Cwtch", "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", @@ -124,9 +115,6 @@ "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:", - "pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...", - "tooltipAddContact": "Add a new contact or conversation", - "titleManageContacts": "Conversations", "titleManageServers": "Manage Servers", "dateNever": "Never", "dateLastYear": "Last Year", @@ -139,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.", "titleManageProfiles": "Manage Cwtch Profiles", "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageContacts": "Conversations", + "tooltipAddContact": "Add a new contact or conversation", "tooltipOpenSettings": "Open the settings pane", - "invalidImportString": "Invalid import string", "contactAlreadyExists": "Contact Already Exists", + "invalidImportString": "Invalid import string", "conversationSettings": "Conversation Settings", "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "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:", - "builddate": "Basado en: %2", - "version": "Versión %1", - "versionTor": "Versión %1 con tor %2", + "todoPlaceholder": "Por hacer...", + "bulletinsBtn": "Boletines", + "radioNoPassword": "Sin cifrado (sin contraseña)", "themeDark": "Oscuro", - "themeLight": "Claro", - "settingTheme": "Tema", - "largeTextLabel": "Grande", - "settingInterfaceZoom": "Nivel de zoom", + "smallTextLabel": "Pequeño", + "loadingTor": "Cargando tor...", + "cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.", + "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", "localePt": "Portugués", "localeFr": "Francés", - "localeEn": "Inglés", - "settingLanguage": "Idioma", - "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", - "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", - "copyBtn": "Copiar", - "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", + "addListItem": "Añadir un nuevo elemento a la lista", + "unblockBtn": "Desbloquear contacto", "joinGroupTab": "Únete a un grupo", - "createGroupTab": "Crear un grupo", - "createGroupBtn": "Crear", + "viewGroupMembershipTooltip": "Ver membresía del grupo", + "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", - "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" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index fdd9808f..66c5b046 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,65 +1,70 @@ { "@@locale": "fr", - "@@last_modified": "2021-11-21T17:42:07+01:00", - "manageKnownServersShort": "Servers", - "manageKnownServersLong": "Manage Known Servers", - "displayNameTooltip": "Veuillez entrer un nom d'usage s'il vous plaît", + "@@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", + "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", - "fieldDescriptionLabel": "Description", - "groupsOnThisServerLabel": "Les groupes dont je fais partie sont hébergés sur ce serveur", - "importLocalServerButton": "Importer %1", "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", "verfiyResumeButton": "Vérifier\/reprendre", - "fileCheckingStatus": "Vérification de l'état du téléchargement", - "fileInterrupted": "Interrompu", "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.", - "encryptedServerDescription": "Le chiffrement d’un 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 n’est 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", - "copyAddress": "Copier l'adresse", + "encryptedServerDescription": "Le chiffrement d’un 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 n’est 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.", "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", "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", "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", + "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", + "saveServerButton": "Enregistrer le serveur", + "enterServerPassword": "Entrez le mot de passe pour déverrouiller le serveur", "editServerTitle": "Modifier le serveur", + "copyAddress": "Copier l'adresse", + "addServerTooltip": "Ajouter un nouveau serveur", "addServerTitle": "Ajouter un serveur", "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.", - "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", - "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", + "messageFileOffered": "Contact vous propose de vous envoyer un fichier", "messageEnableFileSharing": "Activez l'expérience de partage de fichiers pour afficher ce message.", "labelFilesize": "Taille", "labelFilename": "Nom de fichier", "downloadFileButton": "Télécharger", - "openFolderButton": "Ouvrir le dossier", - "retrievingManifestMessage": "Récupération des informations sur le fichier...", + "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.", "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.", "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.", "peerBlockedMessage": "Le contact est bloqué", "peerOfflineMessage": "Le contact est hors ligne, les messages ne peuvent pas être transmis pour le moment.", @@ -69,15 +74,15 @@ "unblockBtn": "Débloquer le contact", "blockUnknownLabel": "Bloquer les pairs inconnus", "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...", - "showMessageButton": "Afficher le message", + "archiveConversation": "Archiver cette conversation", "blockedMessageMessage": "Ce message provient d'un profil que vous avez bloqué.", + "showMessageButton": "Afficher le 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.", + "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", - "addContact": "Ajouter le contact", "contactGoto": "Aller à la conversation avec %1", + "addContact": "Ajouter le contact", "settingUIColumnOptionSame": "Même réglage que pour le mode portrait", "settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble12Ratio": "Double (1:2)", @@ -85,127 +90,158 @@ "settingUIColumnLandscape": "Colonnes de l'interface utilisateur en mode paysage", "settingUIColumnPortrait": "Colonnes de l'interface utilisateur en mode portrait", "localePl": "Polonais", - "tooltipRemoveThisQuotedMessage": "Supprimer le message cité.", "tooltipReplyToThisMessage": "Répondre à ce message", - "tooltipRejectContactRequest": "Refuser cette demande de contact", - "tooltipAcceptContactRequest": "Acceptez cette demande de contact.", - "notificationNewMessageFromGroup": "Nouveau message dans un groupe !", - "notificationNewMessageFromPeer": "Nouveau message d'un contact !", - "tooltipHidePassword": "Masquer le mot de passe", - "tooltipShowPassword": "Afficher le mot de passe", - "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 ?", + "tooltipRemoveThisQuotedMessage": "Supprimer le message cité.", + "deleteProfileConfirmBtn": "Supprimer vraiment le profil ?", + "groupNameLabel": "Nom du groupe", + "defaultGroupName": "Un groupe génial", + "inviteToGroupLabel": "Inviter au groupe", + "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.", "shutdownCwtchTooltip": "Arrêt de Cwtch", - "malformedMessage": "Message mal formé", - "profileDeleteSuccess": "Le profil a été supprimé avec succès", - "debugLog": "Activer le journal de la console de débogage", - "torNetworkStatus": "Statut du réseau Tor", - "addContactFirst": "Ajoutez ou choisissez un contact pour commencer à discuter.", - "createProfileToBegin": "Veuillez créer ou déverrouiller un profil pour commencer", - "nickChangeSuccess": "Le pseudo du profil a été modifié avec succès", - "addServerFirst": "Vous devez ajouter un serveur avant de pouvoir créer un groupe.", - "deleteProfileSuccess": "Le profil a été supprimé avec succès", - "sendInvite": "Envoyer une invitation à un contact ou à un groupe", - "sendMessage": "Envoyer un message", - "cancel": "Annuler", - "resetTor": "Réinitialiser", - "torStatus": "Statut de Tor", - "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 :", + "shutdownCwtchAction": "Arrêt de Cwtch", + "deleteBtn": "Effacer", + "acknowledgedLabel": "Accusé de réception", + "zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)", + "localeIt": "Italien", + "versionTor": "Version %1 avec tor %2", + "version": "Version %1", + "builddate": "Construit le : %2", + "versionBuilddate": "Version : %1 Construite le : %2", + "tooltipAcceptContactRequest": "Acceptez cette demande de contact.", + "tooltipRejectContactRequest": "Refuser cette demande de contact", + "addNewItem": "Ajouter un nouvel élément à la liste", + "localeEs": "Espagnol", + "todoPlaceholder": "À faire...", "pasteAddressToAddContact": "Collez une adresse cwtch, une invitation ou un ensemble de clés ici pour ajouter une nouvelle conversation", - "tooltipAddContact": "Ajouter un nouveau contact ou une nouvelle conversation", - "titleManageContacts": "Conversations", - "titleManageServers": "Gérer les serveurs", - "dateNever": "Jamais", + "addListItem": "Ajouter un nouvel élément de liste", + "cycleMorphsAndroid": "Cliquez pour faire défiler les morphes.\n Appuyez longuement pour réinitialiser.", + "cycleMorphsDesktop": "Cliquez pour faire défiler les morphes.\n Faites un clic droit pour réinitialiser.", + "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", - "dateYesterday": "Hier", - "dateLastMonth": "Le mois dernier", - "dateRightNow": "Maintenant", - "successfullAddedContact": "Ajouté avec succès ", - "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.", - "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.", - "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.", - "titleManageProfiles": "Gérer les profils Cwtch", - "tooltipUnlockProfiles": "Déverrouillez les profils chiffrés en saisissant leur mot de passe.", - "tooltipOpenSettings": "Ouvrez le volet des paramètres", + "dateNever": "Jamais", + "titleManageServers": "Gérer les serveurs", + "inviteToGroup": "Vous avez été invité à rejoindre un groupe :", + "leaveGroup": "Quittez cette conversation", + "reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.", + "yesLeave": "Oui, quittez cette conversation", + "noPasswordWarning": "Ne pas utiliser de mot de passe sur ce compte signifie que toutes les données stockées localement ne seront pas chiffrées.", + "yourDisplayName": "Pseudo", + "currentPasswordLabel": "Mot de passe actuel", + "password1Label": "Mot de passe", + "password2Label": "Saisissez à nouveau le mot de passe", + "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", "contactAlreadyExists": "Le contact existe déjà", - "conversationSettings": "Paramètres de conversation", - "enterCurrentPasswordForDelete": "Veuillez entrer le mot de passe actuel pour supprimer ce profil.", - "enableGroups": "Activer la discussion de groupe", - "experimentsEnabled": "Activer les expériences", - "localeIt": "Italien", - "localeEs": "Espagnol", - "addListItem": "Ajouter un nouvel élément de liste", - "addNewItem": "Ajouter un nouvel élément à la liste", - "todoPlaceholder": "À faire...", - "newConnectionPaneTitle": "Nouvelle connexion", - "networkStatusOnline": "En ligne", - "networkStatusAttemptingTor": "Tentative de connexion au réseau Tor", - "networkStatusDisconnected": "Déconnecté d'Internet, vérifiez votre connexion", - "viewGroupMembershipTooltip": "Afficher les membres du groupe", - "loadingTor": "Chargement de tor...", + "tooltipOpenSettings": "Ouvrez le volet des paramètres", + "tooltipAddContact": "Ajouter un nouveau contact ou une nouvelle conversation", + "titleManageContacts": "Conversations", + "tooltipUnlockProfiles": "Déverrouillez les profils chiffrés en saisissant leur mot de passe.", + "titleManageProfiles": "Gérer les profils Cwtch", + "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.", + "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.", + "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.", + "successfullAddedContact": "Ajouté avec succès ", + "dateRightNow": "Maintenant", + "dateLastMonth": "Le mois dernier", + "dateYesterday": "Hier", + "newPassword": "Nouveau mot de passe", + "chatHistoryDefault": "Cette conversation sera supprimée lorsque Cwtch sera fermé ! L'historique des messages peut être activé pour la conversation via le menu Paramètres en haut à droite.", + "accepted": "Accepté !", + "rejected": "Rejeté !", + "contactSuggestion": "Il s'agit d'une suggestion de contact pour : ", + "sendAnInvitation": "Vous avez envoyé une invitation pour : ", + "torVersion": "Version de Tor", + "torStatus": "Statut de Tor", + "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", "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", - "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", - "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", - "editProfile": "Modifier le profil", - "newProfile": "Nouveau profil", - "defaultProfileName": "Alice", - "profileName": "Pseudo", - "editProfileTitle": "Modifier le profil", - "addProfileTitle": "Ajouter un nouveau profil", - "deleteBtn": "Effacer", "saveBtn": "Sauvegarder", "displayNameLabel": "Pseudo", "addressLabel": "Adresse", @@ -220,45 +256,14 @@ "copiedClipboardNotification": "Copié dans le presse-papier", "copyBtn": "Copier", "pendingLabel": "En attente", - "acknowledgedLabel": "Accusé de réception", "couldNotSendMsgError": "Impossible d'envoyer ce message", "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", - "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", "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...", "postNewBulletinLabel": "Envoyer un 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", - "defaultGroupName": "Un groupe génial", "createGroupTitle": "Créer un groupe" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 113411cd..ece23e55 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,11 @@ { "@@locale": "it", - "@@last_modified": "2021-11-21T17:42:07+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", @@ -10,255 +15,255 @@ "importLocalServerButton": "Import %1", "importLocalServerSelectText": "Select Local Server", "importLocalServerLabel": "Import a locally hosted server", - "savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.", - "newMessagesLabel": "Nuovi messaggi", - "localeRU": "Russo", - "copyServerKeys": "Copia chiavi", - "verfiyResumeButton": "Verifica\/riprendi", - "fileCheckingStatus": "Controllo dello stato del download", - "fileInterrupted": "Interrotto", - "fileSavedTo": "Salvato in", - "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.", - "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.", - "deleteServerConfirmBtn": "Elimina davvero il server", - "deleteServerSuccess": "Server eliminato con successo", - "enterCurrentPasswordForDeleteServer": "Inserisci la password attuale per eliminare questo server", - "copyAddress": "Copia indirizzo", - "settingServersDescription": "L'esperimento dei server di hosting permette di allocare e gestire i server Cwtch", - "settingServers": "Server di hosting", - "enterServerPassword": "Inserisci la password per sbloccare il server", - "unlockProfileTip": "Crea o sblocca un profilo per iniziare!", - "unlockServerTip": "Crea o sblocca un server per iniziare!", - "addServerTooltip": "Aggiungi nuovo server", - "serversManagerTitleShort": "Gestisci i server", - "serversManagerTitleLong": "Server che gestisci", - "saveServerButton": "Salva il server", - "serverAutostartDescription": "Controlla se l'applicazione avvierà automaticamente il server all'avvio", - "serverAutostartLabel": "Avvio automatico", - "serverEnabledDescription": "Avvia o arresta il server", - "serverEnabled": "Server abilitato", - "serverDescriptionDescription": "La tua descrizione del server solo per gestione personale, non sarà mai condivisa", - "serverDescriptionLabel": "Descrizione del server", - "serverAddress": "Indirizzo del server", - "editServerTitle": "Modifica il server", - "addServerTitle": "Aggiungi server", - "titleManageProfilesShort": "Profili", "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", - "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.", - "settingFileSharing": "Condivisione file", - "tooltipSendFile": "Invia file", - "messageFileOffered": "Il contatto offre l'invio di un file", - "messageFileSent": "Hai inviato un file", - "messageEnableFileSharing": "Abilita l'esperimento di condivisione dei file per visualizzare questo messaggio.", - "labelFilesize": "Dimensione", - "labelFilename": "Nome del file", - "downloadFileButton": "Scarica", - "openFolderButton": "Apri cartella", "retrievingManifestMessage": "Recupero delle informazioni sul file in corso...", - "streamerModeLabel": "Modalità Streamer\/Presentazione", - "archiveConversation": "Archivia questa conversazione", - "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", - "dontSavePeerHistory": "Elimina cronologia dei peer", - "unblockBtn": "Sblocca il peer", - "blockUnknownLabel": "Blocca peer sconosciuti", - "blockUnknownConnectionsEnabledDescription": "Le connessioni da contatti sconosciuti sono bloccate. Puoi modificare questa impostazione in Impostazioni", - "networkStatusConnecting": "Connessione alla rete e ai peer ...", - "showMessageButton": "Mostra il messaggio", - "blockedMessageMessage": "Questo messaggio proviene da un profilo che hai bloccato.", - "placeholderEnterMessage": "Scrivi un messaggio...", - "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.", - "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.", - "addContactConfirm": "Aggiungi %1 come contatto", - "addContact": "Aggiungi contatto", - "contactGoto": "Vai alla conversazione con %1", - "settingUIColumnOptionSame": "Stessa impostazione della modalità verticale", - "settingUIColumnDouble14Ratio": "Doppia (1:4)", - "settingUIColumnDouble12Ratio": "Doppia (1:2)", - "settingUIColumnSingle": "Singola", - "settingUIColumnLandscape": "Colonne dell'interfaccia utente in modalità orizzontale", - "settingUIColumnPortrait": "Colonne dell'interfaccia utente in modalità verticale", - "localePl": "Polacco", - "tooltipRemoveThisQuotedMessage": "Rimuovi il messaggio citato.", + "openFolderButton": "Apri cartella", + "downloadFileButton": "Scarica", + "labelFilename": "Nome del file", + "labelFilesize": "Dimensione", + "messageEnableFileSharing": "Abilita l'esperimento di condivisione dei file per visualizzare questo messaggio.", + "messageFileSent": "Hai inviato un file", + "titleManageProfilesShort": "Profili", + "addServerTitle": "Aggiungi server", "tooltipReplyToThisMessage": "Rispondi a questo messaggio", - "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", + "tooltipRemoveThisQuotedMessage": "Rimuovi il messaggio citato.", + "localePl": "Polacco", + "settingUIColumnPortrait": "Colonne dell'interfaccia utente in modalità verticale", + "settingUIColumnLandscape": "Colonne dell'interfaccia utente in modalità orizzontale", + "settingUIColumnSingle": "Singola", + "settingUIColumnDouble12Ratio": "Doppia (1:2)", + "settingUIColumnDouble14Ratio": "Doppia (1:4)", + "settingUIColumnOptionSame": "Stessa impostazione della modalità verticale", + "contactGoto": "Vai alla conversazione con %1", + "addContact": "Aggiungi contatto", + "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", + "tooltipSendFile": "Invia file", + "settingFileSharing": "Condivisione file", + "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.", + "experimentsEnabled": "Abilita esperimenti", "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", - "tooltipAddContact": "Aggiungi un nuovo contatto o conversazione", - "titleManageContacts": "Conversazioni", - "titleManageServers": "Gestisci i server", - "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", + "enableGroups": "Abilita la chat di gruppo", + "enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.", + "conversationSettings": "Impostazioni di conversazione", "invalidImportString": "Importazione stringa non valida", "contactAlreadyExists": "Il contatto esiste già", - "conversationSettings": "Impostazioni di conversazione", - "enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.", - "enableGroups": "Abilita la chat di gruppo", - "experimentsEnabled": "Abilita esperimenti", + "tooltipOpenSettings": "Aprire il pannello delle impostazioni", + "tooltipAddContact": "Aggiungi un nuovo contatto o conversazione", + "titleManageContacts": "Conversazioni", + "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", "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", "localePt": "Portoghese", "localeFr": "Francese", - "localeEn": "Inglese", - "settingLanguage": "Lingua", - "zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)", - "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", + "addListItem": "Aggiungi un nuovo elemento alla lista", + "savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.", + "unblockBtn": "Sblocca il peer", "copiedToClipboardNotification": "Copiato negli Appunti", - "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", - "copyBtn": "Copia", - "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", + "createGroupTitle": "Crea un gruppo", "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.", "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.", - "cycleMorphsAndroid": "Fare clic per scorrere i morph.\nPressione lunga per resettare.", - "cycleCatsDesktop": "Fare clic per scorrere le categorie.\nCliccare con il tasto destro per resettare.", - "cycleCatsAndroid": "Fare clic per scorrere le categorie.\nPressione lunga per resettare.", - "blocked": "Bloccato", - "titlePlaceholder": "titolo...", - "postNewBulletinLabel": "Pubblica un nuovo bollettino", - "newBulletinLabel": "Nuovo bollettino", - "joinGroup": "Unisciti al gruppo", - "createGroup": "Crea un gruppo", - "groupAddr": "Indirizzo", - "invitation": "Invito", - "server": "Server", - "groupName": "Nome del gruppo", - "peerName": "Nome", - "peerAddress": "Indirizzo", - "joinGroupTab": "Unisciti a un gruppo", - "createGroupTab": "Crea un gruppo", - "createGroupBtn": "Crea", - "defaultGroupName": "Gruppo fantastico", - "createGroupTitle": "Crea un gruppo" + "search": "Ricerca...", + "peerBlockedMessage": "Il peer è bloccato", + "peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento", + "copyBtn": "Copia", + "copiedClipboardNotification": "Copiato negli Appunti", + "newGroupBtn": "Crea un nuovo gruppo", + "profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi", + "invitationLabel": "Invito", + "serverInfo": "Informazioni sul server", + "serverConnectivityConnected": "Server connesso", + "serverConnectivityDisconnected": "Server disconnesso", + "membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.", + "dmTooltip": "Clicca per inviare un Messagio Diretto", + "couldNotSendMsgError": "Impossibile inviare questo messaggio", + "acknowledgedLabel": "Riconosciuto", + "pendingLabel": "In corso", + "acceptGroupInviteLabel": "Vuoi accettare l'invito a", + "acceptGroupBtn": "Accetta", + "rejectGroupBtn": "Rifiuta", + "chatBtn": "Chat", + "listsBtn": "Liste", + "bulletinsBtn": "Bollettini", + "puzzleGameBtn": "Gioco di puzzle", + "addressLabel": "Indirizzo", + "displayNameLabel": "Nome visualizzato", + "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" } \ No newline at end of file diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index a8fd1a3e..a5d18aa7 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,11 @@ { "@@locale": "pl", - "@@last_modified": "2021-11-21T17:42:07+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", @@ -10,124 +15,107 @@ "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": "Nowe wiadomości", - "localeRU": "Rosyjski", - "copyServerKeys": "Kopiuj klucze", - "verfiyResumeButton": "Zweryfikuj\/wznów", - "fileCheckingStatus": "Sprawdzanie stanu pobierania", - "fileInterrupted": "Przerwane", - "fileSavedTo": "Zapisano do", - "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.", - "deleteServerConfirmBtn": "Naprawdę usuń serwer", - "deleteServerSuccess": "Pomyślnie usunięto serwer", - "enterCurrentPasswordForDeleteServer": "Wprowadź aktualne hasło, aby usunąć ten serwer", - "copyAddress": "Skopiuj adres", - "settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers", - "settingServers": "Hosting serwerów", - "enterServerPassword": "Wprowadź hasło, aby odblokować serwer", - "unlockProfileTip": "Please create or unlock a profile to begin!", - "unlockServerTip": "Please create or unlock a server to begin!", - "addServerTooltip": "Dodaj nowy serwer", - "serversManagerTitleShort": "Serwery", - "serversManagerTitleLong": "Serwery, które hostujesz", - "saveServerButton": "Zapisz serwer", - "serverAutostartDescription": "Controls if the application will automatically launch the server on start", - "serverAutostartLabel": "Autostart", - "serverEnabledDescription": "Uruchom lub zatrzymaj serwer", - "serverEnabled": "Serwer włączony", - "serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared", - "serverDescriptionLabel": "Opis serwera", - "serverAddress": "Adres serwera", - "editServerTitle": "Edytuj serwer", - "addServerTitle": "Dodaj serwer", - "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", - "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", - "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": "Pokaż wiadomość", - "blockedMessageMessage": "Ta wiadomość pochodzi z profilu, który został przez Ciebie zablokowany.", - "placeholderEnterMessage": "Wpisz wiadomość...", - "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": "Dodaj kontakt %1", - "addContact": "Dodaj kontakt", - "contactGoto": "Przejdź do rozmowy z %1", - "settingUIColumnOptionSame": "Tak samo jak w przypadku trybu portretowego", - "settingUIColumnDouble14Ratio": "Podwójny (1:4)", + "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", + "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", + "unlockProfileTip": "Please create or unlock a profile to begin!", + "unlockServerTip": "Please create or unlock a server to begin!", + "serverAutostartDescription": "Controls if the application will automatically launch the server on start", + "serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared", + "blockUnknownConnectionsEnabledDescription": "Połączenia od nieznanych kontaktów są blokowane. Można to zmienić w Ustawieniach", + "archiveConversation": "Zarchiwizuj tę rozmowę", + "streamerModeLabel": "Tryb streamera\/prezentacji", + "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", + "descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses", + "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.", "settingUIColumnLandscape": "UI Columns in Landscape Mode", "settingUIColumnPortrait": "UI Columns in Portrait Mode", - "localePl": "Polski", - "tooltipRemoveThisQuotedMessage": "Usuń cytowaną wiadomość.", - "tooltipReplyToThisMessage": "Odpowiedz na tę wiadomość", - "tooltipRejectContactRequest": "Odrzuć tę prośbę o kontakt", - "tooltipAcceptContactRequest": "Zaakceptuj tę prośbę o kontakt.", - "notificationNewMessageFromGroup": "Nowa wiadomość w grupie!", - "notificationNewMessageFromPeer": "Nowa wiadomość od kontaktu!", - "tooltipHidePassword": "Ukryj hasło", - "tooltipShowPassword": "Pokaż hasło", - "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.", - "shutdownCwtchAction": "Zamknij Cwtch", "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", - "shutdownCwtchDialogTitle": "Zamknąć Cwtch?", - "shutdownCwtchTooltip": "Zamknij Cwtch", - "malformedMessage": "Źle sformatowana wiadomość", - "profileDeleteSuccess": "Pomyślnie usunięto profil", - "debugLog": "Włącz logowanie debugowania konsoli", - "torNetworkStatus": "Stan sieci Tor", "addContactFirst": "Add or pick a contact to begin chatting.", "createProfileToBegin": "Please create or unlock a profile to begin", - "nickChangeSuccess": "Nick w profilu został zmieniony pomyślnie", "addServerFirst": "You need to add a server before you can create a group", - "deleteProfileSuccess": "Pomyślnie usunięto profil", - "sendInvite": "Wyślij kontakt lub zaproszenie do grupy", - "sendMessage": "Wyślij wiadomość", - "cancel": "Anuluj", - "resetTor": "Reset", - "torStatus": "Status Tor", - "torVersion": "Wersja Tor", - "sendAnInvitation": "You sent an invitation for: ", "contactSuggestion": "This is a contact suggestion for: ", - "rejected": "Odrzucone!", - "accepted": "Przyjęte!", "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": "Nowe hasło", - "yesLeave": "Tak, wyjdź z tej rozmowy", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", - "leaveGroup": "Wyjdź z tej rozmowy", "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": "Zarządzaj serwerami", "dateNever": "Never", "dateLastYear": "Last Year", "dateYesterday": "Yesterday", @@ -139,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.", "titleManageProfiles": "Manage Cwtch Profiles", "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageContacts": "Conversations", + "tooltipAddContact": "Add a new contact or conversation", "tooltipOpenSettings": "Open the settings pane", - "invalidImportString": "Invalid import string", "contactAlreadyExists": "Contact Already Exists", + "invalidImportString": "Invalid import string", "conversationSettings": "Conversation Settings", "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "enableGroups": "Enable Group Chat", - "experimentsEnabled": "Enable Experiments", "localeIt": "Italiana", "localeEs": "Espanol", - "addListItem": "Add a New List Item", - "addNewItem": "Add a new item to the list", "todoPlaceholder": "Todo...", + "addNewItem": "Add a new item to the list", + "addListItem": "Add a New List Item", "newConnectionPaneTitle": "New Connection", "networkStatusOnline": "Online", + "networkStatusConnecting": "Connecting to network and contacts...", "networkStatusAttemptingTor": "Attempting to connect to Tor network", "networkStatusDisconnected": "Disconnected from the internet, check your connection", "viewGroupMembershipTooltip": "View Group Membership", @@ -162,6 +152,7 @@ "builddate": "Built on: %2", "version": "Version %1", "versionTor": "Version %1 with tor %2", + "experimentsEnabled": "Enable Experiments", "themeDark": "Dark", "themeLight": "Light", "settingTheme": "Theme", @@ -172,6 +163,7 @@ "localeFr": "Frances", "localeEn": "English", "settingLanguage": "Language", + "blockUnknownLabel": "Block Unknown Contacts", "zoomLabel": "Interface zoom (mostly affects text and button sizes)", "versionBuilddate": "Version: %1 Built on: %2", "cwtchSettingsTitle": "Cwtch Settings", @@ -195,6 +187,7 @@ "password1Label": "Password", "currentPasswordLabel": "Current Password", "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", "radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "Password", @@ -206,6 +199,11 @@ "editProfileTitle": "Edit Profile", "addProfileTitle": "Add new profile", "deleteBtn": "Delete", + "unblockBtn": "Unblock Contact", + "dontSavePeerHistory": "Delete History", + "savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.", + "savePeerHistory": "Save History", + "blockBtn": "Block Contact", "saveBtn": "Save", "displayNameLabel": "Display Name", "addressLabel": "Address", @@ -219,18 +217,22 @@ "newGroupBtn": "Create new group", "copiedClipboardNotification": "Copied to clipboard", "copyBtn": "Copy", + "peerOfflineMessage": "Contact is offline, messages can't be delivered right now", + "peerBlockedMessage": "Contact is blocked", "pendingLabel": "Pending", "acknowledgedLabel": "Acknowledged", "couldNotSendMsgError": "Could not send this message", "dmTooltip": "Click to DM", "membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.", "addListItemBtn": "Add Item", + "peerNotOnline": "Contact is offline. Applications cannot be used right now.", "searchList": "Search List", "update": "Update", "inviteBtn": "Invite", "inviteToGroupLabel": "Invite to group", "groupNameLabel": "Group name", "viewServerInfo": "Server Info", + "serverNotSynced": "Syncing New Messages (This can take some time)...", "serverSynced": "Synced", "serverConnectivityDisconnected": "Server Disconnected", "serverConnectivityConnected": "Server Connected", @@ -245,11 +247,13 @@ "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", "blocked": "Blocked", + "pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation", "titlePlaceholder": "title...", "postNewBulletinLabel": "Post new bulletin", "newBulletinLabel": "New Bulletin", "joinGroup": "Join group", "createGroup": "Create group", + "addPeer": "Add Contact", "groupAddr": "Address", "invitation": "Invitation", "server": "Server", @@ -258,6 +262,7 @@ "peerAddress": "Address", "joinGroupTab": "Join a group", "createGroupTab": "Create a group", + "addPeerTab": "Add a contact", "createGroupBtn": "Create", "defaultGroupName": "Awesome Group", "createGroupTitle": "Create Group" diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 533631d0..f41f5a06 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,11 @@ { "@@locale": "pt", - "@@last_modified": "2021-11-21T17:42:07+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", @@ -10,7 +15,6 @@ "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", "localeRU": "Russian", "copyServerKeys": "Copy keys", @@ -18,8 +22,8 @@ "fileCheckingStatus": "Checking download status", "fileInterrupted": "Interrupted", "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.", + "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", "deleteServerSuccess": "Successfully deleted server", "enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server", @@ -43,7 +47,6 @@ "editServerTitle": "Edit Server", "addServerTitle": "Add Server", "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.", "settingFileSharing": "File Sharing", "tooltipSendFile": "Send File", @@ -55,21 +58,10 @@ "downloadFileButton": "Download", "openFolderButton": "Open Folder", "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", "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", - "dontSavePeerHistory": "Delete History", - "unblockBtn": "Unblock Contact", - "blockUnknownLabel": "Block Unknown Contacts", "blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings", - "networkStatusConnecting": "Connecting to network and contacts...", "showMessageButton": "Show Message", "blockedMessageMessage": "This message is from a profile you have blocked.", "placeholderEnterMessage": "Type a message...", @@ -93,7 +85,6 @@ "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.", "shutdownCwtchAction": "Shutdown Cwtch", "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", @@ -124,9 +115,6 @@ "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:", - "pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…", - "tooltipAddContact": "Add a new contact or conversation", - "titleManageContacts": "Conversations", "titleManageServers": "Manage Servers", "dateNever": "Never", "dateLastYear": "Last Year", @@ -139,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.", "titleManageProfiles": "Manage Cwtch Profiles", "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", + "titleManageContacts": "Conversations", + "tooltipAddContact": "Add a new contact or conversation", "tooltipOpenSettings": "Open the settings pane", - "invalidImportString": "Invalid import string", "contactAlreadyExists": "Contact Already Exists", + "invalidImportString": "Invalid import string", "conversationSettings": "Conversation Settings", "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "enableGroups": "Enable Group Chat", - "experimentsEnabled": "Enable Experiments", "localeIt": "Italiana", "localeEs": "Espanol", - "addListItem": "Adicionar Item à Lista", - "addNewItem": "Adicionar novo item à lista", "todoPlaceholder": "Afazer…", + "addNewItem": "Adicionar novo item à lista", + "addListItem": "Adicionar Item à Lista", "newConnectionPaneTitle": "New Connection", "networkStatusOnline": "Online", + "networkStatusConnecting": "Connecting to network and contacts...", "networkStatusAttemptingTor": "Attempting to connect to Tor network", "networkStatusDisconnected": "Disconnected from the internet, check your connection", "viewGroupMembershipTooltip": "View Group Membership", @@ -162,6 +152,7 @@ "builddate": "Built on: %2", "version": "Version %1", "versionTor": "Version %1 with tor %2", + "experimentsEnabled": "Enable Experiments", "themeDark": "Dark", "themeLight": "Light", "settingTheme": "Theme", @@ -172,6 +163,7 @@ "localeFr": "Frances", "localeEn": "English", "settingLanguage": "Language", + "blockUnknownLabel": "Block Unknown Contacts", "zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)", "versionBuilddate": "Version: %1 Built on: %2", "cwtchSettingsTitle": "Configurações do Cwtch", @@ -195,6 +187,7 @@ "password1Label": "Password", "currentPasswordLabel": "Current Password", "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", "radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "Password", @@ -206,6 +199,11 @@ "editProfileTitle": "Edit Profile", "addProfileTitle": "Add new profile", "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", "displayNameLabel": "Nome de Exibição", "addressLabel": "Endereço", @@ -219,18 +217,22 @@ "newGroupBtn": "Criar novo grupo", "copiedClipboardNotification": "Copiado", "copyBtn": "Copiar", + "peerOfflineMessage": "Contact is offline, messages can't be delivered right now", + "peerBlockedMessage": "Contact is blocked", "pendingLabel": "Pendente", "acknowledgedLabel": "Confirmada", "couldNotSendMsgError": "Não deu para enviar esta mensagem", "dmTooltip": "Clique para DM", "membershipDescription": "A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo.", "addListItemBtn": "Add Item", + "peerNotOnline": "Contact is offline. Applications cannot be used right now.", "searchList": "Search List", "update": "Update", "inviteBtn": "Convidar", "inviteToGroupLabel": "Convidar ao grupo", "groupNameLabel": "Nome do grupo", "viewServerInfo": "Server Info", + "serverNotSynced": "Syncing New Messages (This can take some time)...", "serverSynced": "Synced", "serverConnectivityDisconnected": "Server Disconnected", "serverConnectivityConnected": "Server Connected", @@ -245,11 +247,13 @@ "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", "blocked": "Blocked", + "pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…", "titlePlaceholder": "título…", "postNewBulletinLabel": "Postar novo boletim", "newBulletinLabel": "Novo Boletim", "joinGroup": "Join group", "createGroup": "Create group", + "addPeer": "Add Contact", "groupAddr": "Address", "invitation": "Invitation", "server": "Server", @@ -258,6 +262,7 @@ "peerAddress": "Address", "joinGroupTab": "Join a group", "createGroupTab": "Create a group", + "addPeerTab": "Add a contact", "createGroupBtn": "Criar", "defaultGroupName": "Grupo incrível", "createGroupTitle": "Criar Grupo" diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index b637956a..d4137344 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,14 +1,39 @@ { "@@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", "copyServerKeys": "Копировать ключи", "verfiyResumeButton": "Проверить\/продолжить", "fileCheckingStatus": "Проверка статуса загрузки", "fileInterrupted": "Прервано", "fileSavedTo": "Сохранить в", - "plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.", "encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.", + "plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.", "deleteServerConfirmBtn": "Точно удалить сервер?", "deleteServerSuccess": "Сервер успешно удален", "enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его", @@ -32,7 +57,6 @@ "editServerTitle": "Изменить сервер", "addServerTitle": "Добавить сервер", "titleManageProfilesShort": "Профили", - "descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена", "descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.", "settingFileSharing": "Передача файлов", "tooltipSendFile": "Отправить файл", @@ -44,22 +68,10 @@ "downloadFileButton": "Загрузить", "openFolderButton": "Открыть папку", "retrievingManifestMessage": "Получение информации о файле...", + "descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена", "streamerModeLabel": "Режим презентации", "archiveConversation": "Отправить чат в архив", - "profileOnionLabel": "Send this address to contacts you want to connect with", - "addPeerTab": "Добавить контакт", - "addPeer": "Добавить контакт", - "peerNotOnline": "Контакт не в сети. Вы не можете связаться с ним пока он не появиться в сети.", - "peerBlockedMessage": "Контакт заблокирован", - "peerOfflineMessage": "Контакт не в сети, сообщения не могут быть отправлены", - "blockBtn": "Заблокировать контакт", - "savePeerHistory": "Хранить исторую", - "savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.", - "dontSavePeerHistory": "Удалить историю", - "unblockBtn": "Разблокировать контакт", - "blockUnknownLabel": "Блокировать неизвестные контакты", "blockUnknownConnectionsEnabledDescription": "Соединения от неизвестных контактов блокируются. Данный параметр можно изменить в настройках", - "networkStatusConnecting": "Подключение к сети и контактам...", "showMessageButton": "Показать сообщения", "blockedMessageMessage": "Это сообщение из заблокированного вами профиля.", "placeholderEnterMessage": "Написать сообщение...", @@ -83,7 +95,6 @@ "notificationNewMessageFromPeer": "Новое сообщение от контакта!", "tooltipHidePassword": "Скрыть пароль", "tooltipShowPassword": "Показать пароль", - "serverNotSynced": "Синхронизация новых сообщений (это может занять некоторое время)...", "groupInviteSettingsWarning": "Вас пригласили присоединиться к группе! Пожалуйста, включите экспериментальную функцию групповые чаты в Настройках, чтобы просмотреть это приглашение.", "shutdownCwtchAction": "Выключить Cwtch", "shutdownCwtchDialog": "Вы уверены, что хотите выключить Cwtch? Это приведет к закрытию всех подключений и выходу из приложения.", @@ -114,9 +125,6 @@ "reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.", "leaveGroup": "Да, оставить этот чат", "inviteToGroup": "Вас пригласили присоединиться к группе:", - "pasteAddressToAddContact": "Вставьте адрес cwtch, приглашение или пакет ключей здесь, чтобы добавить их в контакты", - "tooltipAddContact": "Добавление нового контакта или разговора", - "titleManageContacts": "Разговоры", "titleManageServers": "Управление серверами", "dateNever": "Никогда", "dateLastYear": "Прошлый год", @@ -129,20 +137,21 @@ "descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный част 1 на 1..", "titleManageProfiles": "Управление профилями Cwtch", "tooltipUnlockProfiles": "Разблокировать зашифрованные профили, введя их пароль.", + "titleManageContacts": "Разговоры", + "tooltipAddContact": "Добавление нового контакта или разговора", "tooltipOpenSettings": "Откройте панель настроек", - "invalidImportString": "Недействительная строка импорта", "contactAlreadyExists": "Контакт уже существует", + "invalidImportString": "Недействительная строка импорта", "conversationSettings": "Настройки чата", "enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.", "enableGroups": "Включить Групповые чаты", - "experimentsEnabled": "Включить Экспериментальные функции", "localeIt": "Итальянский", "localeEs": "Испанский", - "addListItem": "Добавить новый элемент", - "addNewItem": "Добавить новый элемент в список", "todoPlaceholder": "Выполняю...", + "addNewItem": "Добавить новый элемент в список", + "addListItem": "Добавить новый элемент", "newConnectionPaneTitle": "Новое соединение", - "networkStatusOnline": "В сети", + "networkStatusConnecting": "Подключение к сети и контактам...", "networkStatusAttemptingTor": "Попытка подключиться к сети Tor", "networkStatusDisconnected": "Нет сети. Проверьте подключение к интернету", "viewGroupMembershipTooltip": "Просмотр членства в группе", @@ -152,6 +161,7 @@ "builddate": "Построен на: %2", "version": "Версия %1", "versionTor": "Версия %1 c tor %2", + "experimentsEnabled": "Включить Экспериментальные функции", "themeDark": "Темная", "themeLight": "Светлая", "settingTheme": "Тема", @@ -162,6 +172,7 @@ "localeFr": "Французский", "localeEn": "Английский", "settingLanguage": "Язык", + "blockUnknownLabel": "Блокировать неизвестные контакты", "zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)", "versionBuilddate": "Версия: %1 Сборка от: %2", "cwtchSettingsTitle": "Настройки Cwtch", @@ -188,20 +199,19 @@ "noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы", "radioNoPassword": "Незашифрованный (без пароля)", "radioUsePassword": "Пароль", - "copiedToClipboardNotification": "Скопировано в буфер обмена", - "copyBtn": "Копировать", "editProfile": "Изменить профиль", "newProfile": "Новый профиль", - "defaultProfileName": "Алиса", "profileName": "Отображаемое имя", "editProfileTitle": "Изменить профиль", "addProfileTitle": "Добавить новый профиль", - "deleteBtn": "Удалить", - "saveBtn": "Сохранить", + "unblockBtn": "Разблокировать контакт", + "dontSavePeerHistory": "Удалить историю", + "savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.", + "savePeerHistory": "Хранить исторую", + "blockBtn": "Заблокировать контакт", "displayNameLabel": "Отображаемое имя", "addressLabel": "Адрес", "puzzleGameBtn": "Puzzle Game", - "bulletinsBtn": "Бюллетень", "listsBtn": "Списки", "chatBtn": "Чат", "rejectGroupBtn": "Отклонить", @@ -209,24 +219,26 @@ "acceptGroupInviteLabel": "Хотите принять приглашение в", "newGroupBtn": "Создать новую группу", "copiedClipboardNotification": "Скопировано в буфер обмена", + "peerOfflineMessage": "Контакт не в сети, сообщения не могут быть отправлены", + "peerBlockedMessage": "Контакт заблокирован", "pendingLabel": "Ожидаемый", "acknowledgedLabel": "Отправлено", "couldNotSendMsgError": "Не удалось отправить это сообщение", "dmTooltip": "Нажмите, чтобы перейти в DM", "membershipDescription": "Ниже приведен список пользователей, отправивших сообщения группе. Этот список может не отражать всех пользователей, имеющих доступ к группе.", "addListItemBtn": "Добавить элемент", + "peerNotOnline": "Контакт не в сети. Вы не можете связаться с ним пока он не появиться в сети.", "searchList": "Список поиска", "update": "Обновить", "inviteBtn": "Пригласить", "inviteToGroupLabel": "Пригласить в группу", - "groupNameLabel": "Имя группы", "viewServerInfo": "Информация о сервере", + "serverNotSynced": "Синхронизация новых сообщений (это может занять некоторое время)...", "serverSynced": "Синхронизировано", "serverConnectivityDisconnected": "Сервер отключен", "serverConnectivityConnected": "Сервер подключен", "serverInfo": "Информация о сервере", "invitationLabel": "Приглашение", - "serverLabel": "Сервер", "search": "Поиск...", "cycleColoursDesktop": "Нажмите, чтобы переключать цвета.\nПравый клик чтобы сбросить.", "cycleColoursAndroid": "Нажмите, чтобы переключать цвета.\nНажмите и удерживайте, чтобы сбросить.", @@ -235,11 +247,13 @@ "cycleCatsDesktop": "Нажмите, чтобы просмотреть категории.\nПравый клик чтобы сбросить.", "cycleCatsAndroid": "Нажмите, чтобы просмотреть категории.\nНажмите и удерживайте, чтобы сбросить.", "blocked": "Заблокировано", + "pasteAddressToAddContact": "Вставьте адрес cwtch, приглашение или пакет ключей здесь, чтобы добавить их в контакты", "titlePlaceholder": "заговолок...", "postNewBulletinLabel": "Опубликовать новый бюллетень", "newBulletinLabel": "Новый бюллетень", "joinGroup": "Вступить в группу", "createGroup": "Создать группу", + "addPeer": "Добавить контакт", "groupAddr": "Адрес", "invitation": "Приглашение", "server": "Сервер", @@ -248,6 +262,7 @@ "peerAddress": "Адрес", "joinGroupTab": "Присоединиться к группе", "createGroupTab": "Создать группу", + "addPeerTab": "Добавить контакт", "createGroupBtn": "Создать", "defaultGroupName": "Замечательная группа", "createGroupTitle": "Создать группу" diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 6668ed52..22d02dfc 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -227,8 +227,8 @@ class _GlobalSettingsViewState extends State { secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()), ), SwitchListTile( - title: Text("Enable Clickable Links", style: TextStyle(color: settings.current().mainTextColor())), - subtitle: Text("The clickable links experiment allows you to click on URLs shared in messages."), + 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) { diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index ae68962b..f05908ed 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -67,7 +67,7 @@ class MessageBubbleState extends State { wdgMessage = SelectableLinkify( text: widget.content + '\u202F', // TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler? - options: LinkifyOptions(humanize: false), + options: LinkifyOptions(humanize: false, removeWww: false, looseUrl: true, defaultToHttps: true), linkifiers: [UrlLinkifier()], onOpen: (link) { _modalOpenLink(context, link); @@ -159,6 +159,7 @@ class MessageBubbleState extends State { onPressed: () async { if (await canLaunch(link.url)) { await launch(link.url); + Navigator.pop(bcontext); } else { throw 'Could not launch $link'; }