Compare commits

...

31 Commits

Author SHA1 Message Date
RuLang c8b4f3ec31 update_2022.07.04_small-fix 2022-07-04 11:39:15 +00:00
RuLang 3405859c8e intl_ru_15.06.2022.arb 2022-06-15 09:27:14 +00:00
Sarah Jamie Lewis 644ae502e5 Merge pull request 'formatting_toolbar' (#475) from formatting_toolbar into trunk
Reviewed-on: cwtch.im/cwtch-ui#475
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-15 03:14:17 +00:00
Sarah Jamie Lewis 7bae6485f7 Fixup Formatting PR (Dans Comments) 2022-06-14 18:44:24 -07:00
Sarah Jamie Lewis 04c335e7a4 formatting toolbar 2022-06-14 18:30:04 -07:00
Sarah Jamie Lewis 3961692817 Nicer Quoted Messages 2022-06-13 10:06:06 -07:00
Sarah Jamie Lewis d703a9636f Fix Contact Message Date not displaying date for day old messages 2022-06-13 09:31:25 -07:00
Dan Ballard e4419366a4 Merge pull request 'Click to scroll on Quoted Message / Shorten Text' (#469) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#469
2022-06-10 23:43:09 +00:00
Sarah Jamie Lewis f848316db9 Fix bug preventing scrolling to unread messages 2022-06-10 15:42:54 -07:00
Sarah Jamie Lewis a5b253f185 Merge pull request 'reply_links' (#470) from reply_links into marcia_fixes
Reviewed-on: cwtch.im/cwtch-ui#470
2022-06-10 21:32:13 +00:00
Sarah Jamie Lewis e7c19c7477 Merge pull request 'show down button in messageview when ever scrolling up' (#471) from show_down into marcia_fixes
Reviewed-on: cwtch.im/cwtch-ui#471
2022-06-10 21:31:10 +00:00
Dan Ballard 59df024867 show down button in messageview when ever scrolling up 2022-06-10 14:28:16 -07:00
Sarah Jamie Lewis 65ff084952 make links in replies clickable 2022-06-10 14:21:40 -07:00
Sarah Jamie Lewis b3e11cfffd remove scroll controller from message view 2022-06-10 12:24:38 -07:00
Sarah Jamie Lewis 0c9be47e17 Click to scroll on Quoted Message / Shorten Text 2022-06-10 12:12:43 -07:00
Dan Ballard 3bb3a8736c Merge pull request 'Fix message view title padding in doublecol view' (#468) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#468
2022-06-10 18:16:31 +00:00
Sarah Jamie Lewis 67850e8e4b Fix message view title padding in doublecol view 2022-06-10 10:40:39 -07:00
Dan Ballard c8e896fa51 Merge pull request 'Modal Menu UI Fixes' (#467) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#467
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-09 23:19:13 +00:00
Sarah Jamie Lewis d1e8f71290 fixes for profile buttons 2022-06-09 14:54:48 -07:00
Sarah Jamie Lewis be8646e805 fix padding 2022-06-09 14:30:38 -07:00
Sarah Jamie Lewis 6d42f2c76c make text bold and add additional padding to contacts modal 2022-06-09 14:28:24 -07:00
Sarah Jamie Lewis 8429907650 modal menus design fixes 2022-06-09 14:26:02 -07:00
Sarah Jamie Lewis c3848553d7 Bugfix when resizing app when menu is open 2022-06-09 13:49:38 -07:00
Sarah Jamie Lewis 3c85b8f59e Merge pull request 'Column-wise contact row (marcia feedback)' (#466) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#466
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-06-09 20:43:10 +00:00
Sarah Jamie Lewis d0e7e6703b Different buttons 2022-06-09 13:40:42 -07:00
Sarah Jamie Lewis 2bc47173f9 more clear contact request 2022-06-09 13:36:59 -07:00
Sarah Jamie Lewis 15c68d8812 remove padding 2022-06-09 13:20:01 -07:00
Sarah Jamie Lewis e76f2883c6 Column-wise contact row (marcia feedback) 2022-06-09 13:10:27 -07:00
Dan Ballard 439b9b874f Merge pull request 'marcia settings fixes' (#462) from marcia_fixes into trunk
Reviewed-on: cwtch.im/cwtch-ui#462
2022-05-31 23:31:27 +00:00
Sarah Jamie Lewis f5393cdb79 Merge branch 'trunk' into marcia_fixes 2022-05-31 23:19:49 +00:00
Sarah Jamie Lewis c0f1b674aa marcia settings fixes 2022-05-31 13:37:32 -07:00
17 changed files with 720 additions and 420 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,20 +29,21 @@ class ContactsView extends StatefulWidget {
// selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, int handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
var unread = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
if (previouslySelected != null) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
}
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected();
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).initialScrollIndex = initialIndex;
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread;
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
// Set last message seen time in backend
Provider.of<FlwtchState>(context, listen: false)
.cwtch
@ -254,61 +255,86 @@ class _ContactsViewState extends State<ContactsView> {
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
padding: EdgeInsets.all(2.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
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(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
message: AppLocalizations.of(context)!.tooltipAddContact,
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.addServerTitle, semanticsLabel: AppLocalizations.of(context)!.addServerTitle),
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addContact,
semanticsLabel: AppLocalizations.of(context)!.addContact,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
_pushAddContact(false);
},
))),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addServerTitle,
semanticsLabel: AppLocalizations.of(context)!.addServerTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled
? () {
_pushAddContact(false);
}
: null,
)),
),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.createGroupTitle,
semanticsLabel: AppLocalizations.of(context)!.createGroupTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled
? () {
_pushAddContact(false);
_pushAddContact(true);
}
: null,
)),
),
Spacer()
]),
))),
SizedBox(
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")),
);
}).toList())),
leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
leading: Icon(Icons.palette, color: settings.current().mainTextColor),
),
ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor)),
@ -188,7 +188,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
softWrap: true,
style: TextStyle(color: settings.current().mainTextColor),
),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
leading: Icon(Icons.stay_primary_landscape, color: settings.current().mainTextColor),
trailing: Container(
width: MediaQuery.of(context).size.width / 4,
child: Container(
@ -406,7 +406,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
},
activeTrackColor: settings.theme.defaultButtonActiveColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor),
secondary: Icon(Icons.photo, color: settings.current().mainTextColor),
),
Visibility(
visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,13 @@
import 'dart:io';
import 'package:cwtch/controllers/open_link_modal.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart';
import 'messagebubbledecorations.dart';
@ -55,7 +52,7 @@ class MessageBubbleState extends State<MessageBubble> {
linkifiers: [UrlLinkifier()],
onOpen: showClickableLinks
? (link) {
_modalOpenLink(context, link);
modalOpenLink(context, link);
}
: null,
//key: Key(myKey),
@ -104,59 +101,4 @@ class MessageBubbleState extends State<MessageBubble> {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
});
}
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(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,9 +13,8 @@ import '../main.dart';
import '../settings.dart';
class MessageList extends StatefulWidget {
ItemScrollController scrollController;
ItemPositionsListener scrollListener;
MessageList(this.scrollController, this.scrollListener);
MessageList(this.scrollListener);
@override
_MessageListState createState() => _MessageListState();
@ -30,7 +29,6 @@ class _MessageListState extends State<MessageList> {
MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
}
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
@ -82,7 +80,7 @@ class _MessageListState extends State<MessageList> {
child: loadMessages
? ScrollablePositionedList.builder(
itemPositionsListener: widget.scrollListener,
itemScrollController: widget.scrollController,
itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController,
initialScrollIndex: initi > 4 ? initi - 4 : 0,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...

View File

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