Compare commits

..

1 Commits

22 changed files with 688 additions and 746 deletions

View File

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

View File

@ -176,6 +176,7 @@ class CwtchNotifier {
seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc()); seenMessageCallback!(data["ProfileOnion"]!, identifier, DateTime.now().toUtc());
} }
print("New Message from peer...");
if (notification == "SimpleEvent") { if (notification == "SimpleEvent") {
notificationManager.notify(notificationSimple ?? "New Message", "", 0); notificationManager.notify(notificationSimple ?? "New Message", "", 0);
} else if (notification == "ContactInfo") { } else if (notification == "ContactInfo") {

View File

@ -312,6 +312,182 @@ class MaterialLocalizationLu extends MaterialLocalizations {
@override @override
String get viewLicensesButtonLabel => 'LIZENZEN ANZEIGEN'; String get viewLicensesButtonLabel => 'LIZENZEN ANZEIGEN';
// ***** NEW *****
@override
String get keyboardKeyAlt => 'Datum';
@override
String get keyboardKeyAltGraph => 'Datum';
@override
String get keyboardKeyBackspace => 'Datum';
@override
String get keyboardKeyCapsLock => 'Datum';
@override
String get keyboardKeyChannelDown => 'Datum';
@override
String get keyboardKeyChannelUp => 'Datum';
@override
String get keyboardKeyControl => 'Datum';
@override
String get keyboardKeyDelete => 'Datum';
@override
String get keyboardKeyEisu => 'Datum';
@override
String get keyboardKeyEject => 'Datum';
@override
String get keyboardKeyEnd => 'Datum';
@override
String get keyboardKeyEscape => 'Datum';
@override
String get keyboardKeyFn => 'Datum';
@override
String get keyboardKeyHangulMode => 'Datum';
@override
String get keyboardKeyHanjaMode => 'Datum';
@override
String get keyboardKeyHankaku => 'Datum';
@override
String get keyboardKeyHiragana => 'Datum';
@override
String get keyboardKeyHiraganaKatakana => 'Datum';
@override
String get keyboardKeyHome => 'Datum';
@override
String get keyboardKeyInsert => 'Datum';
@override
String get keyboardKeyKanaMode => 'Datum';
@override
String get keyboardKeyKanjiMode => 'Datum';
@override
String get keyboardKeyKatakana => 'Datum';
@override
String get keyboardKeyMeta => 'Datum';
@override
String get keyboardKeyMetaMacOs => 'Datum';
@override
String get keyboardKeyMetaWindows => 'Datum';
@override
String get keyboardKeyNumLock => 'Datum';
@override
String get keyboardKeyNumpad1 => 'Datum';
@override
String get keyboardKeyNumpad2 => 'Datum';
@override
String get keyboardKeyNumpad3 => 'Datum';
@override
String get keyboardKeyNumpad4 => 'Datum';
@override
String get keyboardKeyNumpad5 => 'Datum';
@override
String get keyboardKeyNumpad6 => 'Datum';
@override
String get keyboardKeyNumpad7 => 'Datum';
@override
String get keyboardKeyNumpad8 => 'Datum';
@override
String get keyboardKeyNumpad9 => 'Datum';
@override
String get keyboardKeyNumpad0 => 'Datum';
@override
String get keyboardKeyNumpadAdd => 'Datum';
@override
String get keyboardKeyNumpadComma => 'Datum';
@override
String get keyboardKeyNumpadDecimal => 'Datum';
@override
String get keyboardKeyNumpadDivide => 'Datum';
@override
String get keyboardKeyNumpadEnter => 'Datum';
@override
String get keyboardKeyNumpadEqual => 'Datum';
@override
String get keyboardKeyNumpadMultiply => 'Datum';
@override
String get keyboardKeyNumpadParenLeft => 'Datum';
@override
String get keyboardKeyNumpadParenRight => 'Datum';
@override
String get keyboardKeyNumpadSubtract => 'Datum';
@override
String get keyboardKeyPageDown => 'Datum';
@override
String get keyboardKeyPageUp => 'Datum';
@override
String get keyboardKeyPower => 'Datum';
@override
String get keyboardKeyPowerOff => 'Datum';
@override
String get keyboardKeyPrintScreen => 'Datum';
@override
String get keyboardKeyRomaji => 'Datum';
@override
String get keyboardKeyScrollLock => 'Datum';
@override
String get keyboardKeySelect => 'Datum';
@override
String get keyboardKeySpace => 'Datum';
@override
String get keyboardKeyZenkaku => 'Datum';
@override
String get keyboardKeyZenkakuHankaku => 'Datum';
@override @override
String aboutListTileTitle(String applicationName) { String aboutListTileTitle(String applicationName) {
return aboutListTileTitleRaw.replaceFirst("$applicationName", applicationName); return aboutListTileTitleRaw.replaceFirst("$applicationName", applicationName);

View File

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

View File

@ -1,7 +1,6 @@
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'message.dart'; import 'message.dart';
import 'messagecache.dart'; import 'messagecache.dart';
@ -45,7 +44,6 @@ class ContactInfoState extends ChangeNotifier {
late Map<String, GlobalKey<MessageRowState>> keys; late Map<String, GlobalKey<MessageRowState>> keys;
int _newMarkerMsgIndex = -1; int _newMarkerMsgIndex = -1;
late MessageCache messageCache; late MessageCache messageCache;
ItemScrollController messageScrollController = new ItemScrollController();
// todo: a nicer way to model contacts, groups and other "entities" // todo: a nicer way to model contacts, groups and other "entities"
late bool _isGroup; late bool _isGroup;

View File

@ -64,8 +64,6 @@ class FileMessage extends Message {
} }
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,
width: 50,
height: 50,
child: FileBubble( child: FileBubble(
nameSuggestion, nameSuggestion,
rootHash, rootHash,
@ -73,7 +71,6 @@ class FileMessage extends Message {
fileSize, fileSize,
isAuto: metadata.isAuto, isAuto: metadata.isAuto,
interactive: false, interactive: false,
isPreview: true,
)); ));
}); });
} }

View File

@ -1,12 +1,17 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:cwtch/widgets/quotedmessage.dart'; import 'package:cwtch/widgets/quotedmessage.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../main.dart';
import '../messagecache.dart';
import '../profile.dart';
class QuotedMessageStructure { class QuotedMessageStructure {
final String quotedHash; final String quotedHash;
final String body; final String body;
@ -29,14 +34,8 @@ class QuotedMessage extends Message {
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
try { try {
dynamic message = jsonDecode( dynamic message = jsonDecode(this.content);
this.content, return Text(message["body"]);
);
var content = message["body"];
return Text(
content,
overflow: TextOverflow.ellipsis,
);
} catch (e) { } catch (e) {
return MalformedBubble(); return MalformedBubble();
} }

View File

@ -1,5 +1,3 @@
import 'dart:math';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart'; import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
@ -21,10 +19,7 @@ class TextMessage extends Message {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
return Text( return SelectableText(this.content);
this.content,
overflow: TextOverflow.ellipsis,
);
}); });
} }

View File

@ -10,6 +10,8 @@ import 'package:desktop_notifications/desktop_notifications.dart' as linux_notif
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
import 'package:flutter_local_notifications_linux/src/model/hint.dart'; import 'package:flutter_local_notifications_linux/src/model/hint.dart';
import 'package:flutter_local_notifications_linux/src/model/icon.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -126,32 +128,80 @@ class NotificationPayload {
class NixNotificationManager implements NotificationsManager { class NixNotificationManager implements NotificationsManager {
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
late Future<void> Function(String, int) notificationSelectConvo; late Future<void> Function(String, int) notificationSelectConvo;
late String linuxAssetsPath;
// Cwtch can install in non flutter supported ways on linux, this code detects where the assets are on Linux
Future<String> detectLinuxAssetsPath() async {
//var devStat = FileStat.stat("assets");
//var localStat = FileStat.stat("data/flutter_assets");
var homeStat = FileStat.stat((Platform.environment["HOME"] ?? "") + "/.local/share/cwtch/data/flutter_assets");
var rootStat = FileStat.stat("/usr/share/cwtch/data/flutter_assets");
/*if ((await devStat).type == FileSystemEntityType.directory) {
return Directory.current.path; //appPath;
} else if ((await localStat).type == FileSystemEntityType.directory) {
return path.join(Directory.current.path, "data/flutter_assets/");
} else */if ((await homeStat).type == FileSystemEntityType.directory) {
return (Platform.environment["HOME"] ?? "") + "/.local/share/cwtch/data/flutter_assets/";
} else if ((await rootStat).type == FileSystemEntityType.directory) {
return "/usr/share/cwtch/data/flutter_assets/";
}
return "";
}
NixNotificationManager(Future<void> Function(String, int) notificationSelectConvo) { NixNotificationManager(Future<void> Function(String, int) notificationSelectConvo) {
this.notificationSelectConvo = notificationSelectConvo; this.notificationSelectConvo = notificationSelectConvo;
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(defaultPresentSound: false);
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification', defaultIcon: AssetsLinuxIcon('assets/knott.png'), defaultSuppressSound: true);
final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux);
scheduleMicrotask(() async { scheduleMicrotask(() async {
if (Platform.isLinux) {
linuxAssetsPath = await detectLinuxAssetsPath();
print("NixNotificationManager found LinuxAssetsPath!: $linuxAssetsPath");
} else {
linuxAssetsPath = "";
}
final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(defaultPresentSound: false);
var linuxIcon = FilePathLinuxIcon( path.join(linuxAssetsPath, 'assets/knott.png'));
print("NixNotificationManager make linux settings");
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification', defaultIcon: linuxIcon, defaultSuppressSound: true);
print("NixNotificationManager InitializationSettings");
final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux);
print("NixNotificationManager mac req perms");
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions( flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
alert: true, alert: true,
badge: false, badge: false,
sound: false, sound: false,
); );
print("NixNotificationManager initialize...");
await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification); await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: selectNotification);
print("NixNotificationManager initialized!!!");
}); });
} }
Future<void> notify(String message, String profile, int conversationId) async { Future<void> notify(String message, String profile, int conversationId) async {
print("notify if !globalAppState.focus so do? ${!globalAppState.focus}");
if (!globalAppState.focus) { if (!globalAppState.focus) {
print("do notify!");
// Warning: Only use title field on Linux, body field will render links as clickable // Warning: Only use title field on Linux, body field will render links as clickable
await flutterLocalNotificationsPlugin.show(0, message, '', NotificationDetails(linux: LinuxNotificationDetails(suppressSound: true, category: LinuxNotificationCategory.imReceived())), await flutterLocalNotificationsPlugin.show(0, message, '',
NotificationDetails(linux: LinuxNotificationDetails(suppressSound: true, category: LinuxNotificationCategory.imReceived(), icon: FilePathLinuxIcon(path.join(linuxAssetsPath, 'assets/knott.png')))),
payload: jsonEncode(NotificationPayload(profile, conversationId))); payload: jsonEncode(NotificationPayload(profile, conversationId)));
print("done notify");
} }
} }
@ -168,7 +218,7 @@ class NixNotificationManager implements NotificationsManager {
NotificationsManager newDesktopNotificationsManager(Future<void> Function(String profileOnion, int convoId) notificationSelectConvo) { NotificationsManager newDesktopNotificationsManager(Future<void> Function(String profileOnion, int convoId) notificationSelectConvo) {
if (Platform.isLinux && !Platform.isAndroid) { if (Platform.isLinux && !Platform.isAndroid) {
try { try {
return LinuxNotificationsManager(notificationSelectConvo); return NixNotificationManager(notificationSelectConvo);
} catch (e) { } catch (e) {
EnvironmentConfig.debugLog("Failed to create LinuxNotificationManager. Switching off notifications."); EnvironmentConfig.debugLog("Failed to create LinuxNotificationManager. Switching off notifications.");
} }

View File

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

View File

@ -29,21 +29,20 @@ class ContactsView extends StatefulWidget {
// selectConversation can be called from anywhere to set the active conversation // selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, int handle) { void selectConversation(BuildContext context, int handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts // requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
var unread = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages; var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation; var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
if (previouslySelected != null) { if (previouslySelected != null) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected(); Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
} }
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected(); Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected();
// triggers update in Double/TripleColumnView // triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread; Provider.of<AppState>(context, listen: false).initialScrollIndex = initialIndex;
Provider.of<AppState>(context, listen: false).selectedConversation = handle; Provider.of<AppState>(context, listen: false).selectedConversation = handle;
Provider.of<AppState>(context, listen: false).selectedIndex = null; Provider.of<AppState>(context, listen: false).selectedIndex = null;
Provider.of<AppState>(context, listen: false).hoveredIndex = -1; Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
// if in singlepane mode, push to the stack // if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context); var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle); if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
// Set last message seen time in backend // Set last message seen time in backend
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
@ -255,86 +254,61 @@ class _ContactsViewState extends State<ContactsView> {
height: 200, // bespoke value courtesy of the [TextField] docs height: 200, // bespoke value courtesy of the [TextField] docs
child: Center( child: Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(2.0), padding: EdgeInsets.all(10.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
children: <Widget>[ children: <Widget>[
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip(
message: AppLocalizations.of(context)!.tooltipAddContact,
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.addContact, semanticsLabel: AppLocalizations.of(context)!.addContact),
onPressed: () {
_pushAddContact(false);
},
))),
Spacer()
]),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Expanded( Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip( child: Tooltip(
message: AppLocalizations.of(context)!.tooltipAddContact, message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( child: Text(AppLocalizations.of(context)!.addServerTitle, semanticsLabel: AppLocalizations.of(context)!.addServerTitle),
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addContact,
semanticsLabel: AppLocalizations.of(context)!.addContact,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
_pushAddContact(false);
},
))),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addServerTitle,
semanticsLabel: AppLocalizations.of(context)!.addServerTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled
? () {
_pushAddContact(false);
}
: null,
)),
),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.createGroupTitle,
semanticsLabel: AppLocalizations.of(context)!.createGroupTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled onPressed: groupsEnabled
? () { ? () {
_pushAddContact(true); _pushAddContact(false);
} }
: null, : null,
))), )),
),
Spacer()
]),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.createGroupTitle, semanticsLabel: AppLocalizations.of(context)!.createGroupTitle),
onPressed: groupsEnabled
? () {
_pushAddContact(true);
}
: null,
))),
Spacer()
]),
], ],
))), ))),
))); )));

View File

@ -161,7 +161,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
child: Text(getThemeName(context, themeId)), //"ddi_$themeId", key: Key("ddi_$themeId")), child: Text(getThemeName(context, themeId)), //"ddi_$themeId", key: Key("ddi_$themeId")),
); );
}).toList())), }).toList())),
leading: Icon(Icons.palette, color: settings.current().mainTextColor), leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
), ),
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor)), title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor)),
@ -188,7 +188,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
softWrap: true, softWrap: true,
style: TextStyle(color: settings.current().mainTextColor), style: TextStyle(color: settings.current().mainTextColor),
), ),
leading: Icon(Icons.stay_primary_landscape, color: settings.current().mainTextColor), leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
trailing: Container( trailing: Container(
width: MediaQuery.of(context).size.width / 4, width: MediaQuery.of(context).size.width / 4,
child: Container( child: Container(
@ -406,7 +406,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
}, },
activeTrackColor: settings.theme.defaultButtonActiveColor, activeTrackColor: settings.theme.defaultButtonActiveColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor, inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.photo, color: settings.current().mainTextColor), secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor),
), ),
Visibility( Visibility(
visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid, visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid,

View File

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

View File

@ -140,7 +140,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
CwtchButtonTextField( CwtchButtonTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion), controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion),
onPressed: _copyOnion, onPressed: _copyOnion,
icon: Icon(CwtchIcons.address_copy_2), icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context)!.copyBtn, tooltip: AppLocalizations.of(context)!.copyBtn,
) )
]), ]),

View File

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

View File

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

View File

@ -25,9 +25,8 @@ class FileBubble extends StatefulWidget {
final int fileSize; final int fileSize;
final bool interactive; final bool interactive;
final bool isAuto; final bool isAuto;
final bool isPreview;
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true, this.isPreview = false}); FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true});
@override @override
FileBubbleState createState() => FileBubbleState(); FileBubbleState createState() => FileBubbleState();
@ -45,22 +44,6 @@ class FileBubbleState extends State<FileBubble> {
super.initState(); super.initState();
} }
Widget getPreview(context) {
return Image.file(
myFile!,
cacheWidth: (MediaQuery.of(context).size.width * 0.6).floor(),
// limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.30,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
@ -126,12 +109,6 @@ class FileBubbleState extends State<FileBubble> {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle; senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
} }
} }
// we don't preview a non downloaded file...
if (widget.isPreview && myFile != null) {
return getPreview(context);
}
return LayoutBuilder(builder: (bcontext, constraints) { return LayoutBuilder(builder: (bcontext, constraints) {
var wdgSender = Visibility( var wdgSender = Visibility(
visible: widget.interactive, visible: widget.interactive,
@ -156,7 +133,21 @@ class FileBubbleState extends State<FileBubble> {
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
child: Padding(padding: EdgeInsets.all(1.0), child: getPreview(context)), child: Padding(
padding: EdgeInsets.all(1.0),
child: Image.file(
myFile!,
cacheWidth: (MediaQuery.of(bcontext).size.width * 0.6).floor(),
// limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(bcontext).size.height * 0.30,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
)),
onTap: () { onTap: () {
pop(bcontext, myFile!, wdgMessage); pop(bcontext, myFile!, wdgMessage);
}, },

View File

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

View File

@ -13,8 +13,9 @@ import '../main.dart';
import '../settings.dart'; import '../settings.dart';
class MessageList extends StatefulWidget { class MessageList extends StatefulWidget {
ItemScrollController scrollController;
ItemPositionsListener scrollListener; ItemPositionsListener scrollListener;
MessageList(this.scrollListener); MessageList(this.scrollController, this.scrollListener);
@override @override
_MessageListState createState() => _MessageListState(); _MessageListState createState() => _MessageListState();
@ -29,6 +30,7 @@ class _MessageListState extends State<MessageList> {
MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache; MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!); ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
} }
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex; var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup; bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated"; bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
@ -80,7 +82,7 @@ class _MessageListState extends State<MessageList> {
child: loadMessages child: loadMessages
? ScrollablePositionedList.builder( ? ScrollablePositionedList.builder(
itemPositionsListener: widget.scrollListener, itemPositionsListener: widget.scrollListener,
itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController, itemScrollController: widget.scrollController,
initialScrollIndex: initi > 4 ? initi - 4 : 0, initialScrollIndex: initi > 4 ? initi - 4 : 0,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages, itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...

View File

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

View File

@ -28,7 +28,7 @@ packages:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.6" version: "3.1.11"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -154,7 +154,7 @@ packages:
name: collection name: collection
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -203,7 +203,7 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
ffi: ffi:
dependency: "direct main" dependency: "direct main"
description: description:
@ -259,16 +259,16 @@ packages:
flutter_local_notifications: flutter_local_notifications:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications path: "/home/dan/src/openprivacy/flutter_local_notifications/flutter_local_notifications"
url: "https://pub.dartlang.org" relative: false
source: hosted source: path
version: "9.3.2" version: "9.5.3+1"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: "direct overridden"
description: description:
name: flutter_local_notifications_linux path: "/home/dan/src/openprivacy/flutter_local_notifications/flutter_local_notifications_linux"
url: "https://pub.dartlang.org" relative: false
source: hosted source: path
version: "0.4.2" version: "0.4.2"
flutter_local_notifications_platform_interface: flutter_local_notifications_platform_interface:
dependency: transitive dependency: transitive
@ -385,7 +385,7 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.4"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@ -407,6 +407,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -490,7 +497,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.1"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -553,7 +560,7 @@ packages:
name: platform name: platform
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.1.0"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -635,7 +642,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -684,7 +691,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.4.9"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:
@ -768,14 +775,14 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.3.0" version: "8.2.2"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -840,5 +847,5 @@ packages:
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.15.0 <3.0.0" dart: ">=2.17.0-0 <3.0.0"
flutter: ">=2.5.0" flutter: ">=2.5.0"

View File

@ -44,9 +44,17 @@ dependencies:
window_manager: ^0.1.4 window_manager: ^0.1.4
# notification plugins # notification plugins
win_toast: ^0.0.2 win_toast: ^0.0.2
flutter_local_notifications: 9.3.2 # 9.3.2
flutter_local_notifications:
path: /home/dan/src/openprivacy/flutter_local_notifications/flutter_local_notifications
desktop_notifications: ^0.6.3 desktop_notifications: ^0.6.3
dependency_overrides:
flutter_local_notifications_linux:
path: /home/dan/src/openprivacy/flutter_local_notifications/flutter_local_notifications_linux
dev_dependencies: dev_dependencies:
msix: ^2.1.3 msix: ^2.1.3
flutter_gherkin: ^3.0.0-rc.9 flutter_gherkin: ^3.0.0-rc.9