diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b7b92848b..851ee8d2bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ All user visible changes to this project will be documented in this file. This p ### Changed - UI: + - Chat page: + - Redesigned message delete popups. ([#1291], [#1268]) - Media panel: - Default order of call buttons in dock. ([#1294], [#1263]) @@ -26,8 +28,10 @@ All user visible changes to this project will be documented in this file. This p - Inability to proceed to recover access with username not being empty. ([#1285]) [#1263]: /../../issue/1263 +[#1268]: /../../issue/1268 [#1280]: /../../pull/1280 [#1285]: /../../pull/1285 +[#1291]: /../../pull/1291 [#1294]: /../../pull/1294 diff --git a/assets/icons/delete19_white.svg b/assets/icons/delete19_white.svg index b832b5b1121..13478295ea0 100644 --- a/assets/icons/delete19_white.svg +++ b/assets/icons/delete19_white.svg @@ -1 +1,14 @@ - + + + + + + + + + + + + + + diff --git a/assets/l10n/en-US.ftl b/assets/l10n/en-US.ftl index 11473c1d14e..692567bc787 100644 --- a/assets/l10n/en-US.ftl +++ b/assets/l10n/en-US.ftl @@ -545,6 +545,7 @@ label_ago_date = {$years -> label_all = All label_all_chats_and_groups = All chats and groups label_all_session_except_current_terminated = All sessions except this one will be terminated +label_also_delete_for_everyone = Also delete for everyone label_always_muted = Always muted label_any_button_or_combination = Any button or combination label_app_background = Application background @@ -656,10 +657,8 @@ label_delete_account = Delete account label_delete_chat = Delete chat label_delete_chats = Delete chats label_delete_email = Delete E-mail -label_delete_for_everyone = Delete for everyone -label_delete_for_me = Delete for me -label_delete_message = Delete the message? -label_delete_messages = Delete the messages? +label_delete_message = Delete the message +label_delete_messages = Delete the messages label_delivered = Delivered label_desktop_apps = Desktop apps label_device_by_default = By default - {$device} @@ -774,8 +773,6 @@ label_media_section_hint = Microphone, speaker, camera label_media_settings = Media settings label_message = Message label_message_editing = Message editing -label_message_will_deleted_for_you = The message will be deleted only for you. -label_messages_will_deleted_for_you = The messages will be deleted only for you. label_microphone_changed = Microphone has been changed to {$microphone} label_mobile_apps = Mobile apps label_money = Money @@ -972,7 +969,9 @@ label_terminate_sessions = Terminate session(s) label_terms_and_privacy_policy = Terms & Privacy policy label_text_status = Text status label_text_status_description = 25 symbols max +label_these_messages_will_be_deleted_only_for_you = These messages will be deleted only for you label_this_device = This device +label_this_message_will_be_deleted_only_for_you = This message will be deleted only for you label_to_restore_chat_use_search = To restore the chat, please, use the search. label_to_restore_chats_use_search = To restore the chats, please, use the search. label_typing = Typing diff --git a/assets/l10n/ru-RU.ftl b/assets/l10n/ru-RU.ftl index eef345d5389..5b0ed46886d 100644 --- a/assets/l10n/ru-RU.ftl +++ b/assets/l10n/ru-RU.ftl @@ -565,6 +565,7 @@ label_ago_date = {$years -> label_all = Все label_all_chats_and_groups = Все чаты и группы label_all_session_except_current_terminated = Все сессии, кроме текущей, будут завершены +label_also_delete_for_everyone = Также удалить для всех label_always_muted = Заглушённые чаты label_any_button_or_combination = Любая кнопка или комбинация label_app_background = Фон приложения @@ -678,10 +679,8 @@ label_delete_account = Удалить аккаунт label_delete_chat = Удалить чат label_delete_chats = Удалить чаты label_delete_email = Удалить E-mail -label_delete_for_everyone = Удалить для всех -label_delete_for_me = Удалить для меня -label_delete_message = Удалить сообщение? -label_delete_messages = Удалить сообщения? +label_delete_message = Удалить сообщение +label_delete_messages = Удалить сообщения label_delivered = Доставлено label_desktop_apps = Десктопные приложения label_device_by_default = По умолчанию - {$device} @@ -796,8 +795,6 @@ label_media_section_hint = Микрофон, спикер, камера label_media_settings = Настройки медиа label_message = Сообщение label_message_editing = Редактирование сообщения -label_message_will_deleted_for_you = Сообщение будет удалено только для Вас. -label_messages_will_deleted_for_you = Сообщения будут удалены только для Вас. label_microphone_changed = Микрофон был изменён на {$microphone} label_mobile_apps = Мобильные приложения label_money = Деньги @@ -997,7 +994,9 @@ label_terminate_sessions = Завершить сессию(-ии) label_terms_and_privacy_policy = Условия и политика конфиденциальности label_text_status = Текстовый статус label_text_status_description = Максимум 25 символов +label_these_messages_will_be_deleted_only_for_you = Эти сообщения будут удалены только у Вас label_this_device = Это устройство +label_this_message_will_be_deleted_only_for_you = Это сообщение будет удалено только у Вас label_to_restore_chat_use_search = Чтобы восстановить чат, пожалуйста, воспользуйтесь поиском. label_to_restore_chats_use_search = Чтобы восстановить чаты, пожалуйста, воспользуйтесь поиском. label_typing = Печатает diff --git a/lib/ui/page/home/page/chat/view.dart b/lib/ui/page/home/page/chat/view.dart index 581f6af588e..65a05b9e93b 100644 --- a/lib/ui/page/home/page/chat/view.dart +++ b/lib/ui/page/home/page/chat/view.dart @@ -43,11 +43,11 @@ import '/ui/page/call/widget/conditional_backdrop.dart'; import '/ui/page/call/widget/fit_view.dart'; import '/ui/page/home/widget/app_bar.dart'; import '/ui/page/home/widget/avatar.dart'; -import '/ui/page/home/widget/confirm_dialog.dart'; import '/ui/page/home/widget/highlighted_container.dart'; import '/ui/page/home/widget/unblock_button.dart'; import '/ui/widget/animated_button.dart'; import '/ui/widget/animated_switcher.dart'; +import '/ui/widget/checkbox_button.dart'; import '/ui/widget/context_menu/menu.dart'; import '/ui/widget/context_menu/region.dart'; import '/ui/widget/future_or_builder.dart'; @@ -1413,67 +1413,78 @@ class ChatView extends StatelessWidget { enabled: canDelete, onPressed: canDelete ? () async { - final bool deletable = - c.chat?.chat.value.isMonolog == true || - c.selected.every((e) { - if (e is ChatMessageElement) { - return e.item.value.author.id == c.me && - c.chat?.chat.value.isRead( - e.item.value, - c.me, - ) == - false; - } else if (e is ChatForwardElement) { - return e.authorId == c.me && - c.chat?.chat.value.isRead( - e.forwards.first.value, - c.me, - ) == - false; - } else if (e is ChatInfoElement) { - return false; - } else if (e is ChatCallElement) { - return false; - } + final bool isMonolog = + c.chat?.chat.value.isMonolog ?? false; + + final bool deletable = c.selected.every((e) { + if (e is ChatMessageElement) { + return e.item.value.author.id == c.me && + c.chat?.chat.value.isRead( + e.item.value, + c.me, + ) == + false; + } else if (e is ChatForwardElement) { + return e.authorId == c.me && + c.chat?.chat.value.isRead( + e.forwards.first.value, + c.me, + ) == + false; + } else if (e is ChatInfoElement) { + return false; + } else if (e is ChatCallElement) { + return false; + } - return false; - }); + return false; + }); - final result = await ConfirmDialog.show( - context, - title: c.selected.length > 1 + bool deleteForAll = false; + + final bool? pressed = await MessagePopup.alert( + c.selected.length > 1 ? 'label_delete_messages'.l10n : 'label_delete_message'.l10n, - description: deletable - ? null - : c.selected.length > 1 - ? 'label_messages_will_deleted_for_you'.l10n - : 'label_message_will_deleted_for_you'.l10n, - initial: 1, - variants: [ - ConfirmDialogVariant( - key: const Key('HideForMe'), - label: 'label_delete_for_me'.l10n, - onProceed: () async { - return await Future.wait( - c.selected.asItems.map(c.hideChatItem), - ); - }, - ), - if (deletable) - ConfirmDialogVariant( - key: const Key('DeleteForAll'), - label: 'label_delete_for_everyone'.l10n, - onProceed: () async { - return await Future.wait( - c.selected.asItems.map(c.deleteMessage), + description: [ + if (!deletable && !isMonolog) + TextSpan( + text: c.selected.length > 1 + ? 'label_these_messages_will_be_deleted_only_for_you' + .l10n + : 'label_this_message_will_be_deleted_only_for_you' + .l10n, + ), + ], + additional: [ + if (deletable && !isMonolog) + StatefulBuilder( + builder: (context, setState) { + return RowCheckboxButton( + key: const Key('DeleteForAll'), + label: 'label_also_delete_for_everyone' + .l10n, + value: deleteForAll, + onPressed: (e) => + setState(() => deleteForAll = e), ); }, ), ], + button: MessagePopup.deleteButton, ); - if (result != null) { + if (pressed ?? false) { + if (deletable && (isMonolog || deleteForAll)) { + await Future.wait( + c.selected.asItems.map(c.deleteMessage), + ); + } else { + await Future.wait( + c.selected.asItems.map(c.hideChatItem), + ); + } + c.selecting.value = false; } } diff --git a/lib/ui/page/home/page/chat/widget/chat_forward.dart b/lib/ui/page/home/page/chat/widget/chat_forward.dart index 973ae3cf59a..a27b69e90c5 100644 --- a/lib/ui/page/home/page/chat/widget/chat_forward.dart +++ b/lib/ui/page/home/page/chat/widget/chat_forward.dart @@ -45,13 +45,14 @@ import '/ui/page/call/widget/fit_view.dart'; import '/ui/page/home/page/chat/controller.dart'; import '/ui/page/home/page/chat/forward/view.dart'; import '/ui/page/home/widget/avatar.dart'; -import '/ui/page/home/widget/confirm_dialog.dart'; import '/ui/page/home/widget/gallery_popup.dart'; +import '/ui/widget/checkbox_button.dart'; import '/ui/widget/context_menu/menu.dart'; import '/ui/widget/context_menu/region.dart'; import '/ui/widget/future_or_builder.dart'; import '/ui/widget/svg/svg.dart'; import '/ui/widget/widget_button.dart'; +import '/util/message_popup.dart'; import '/util/platform_utils.dart'; import 'animated_offset.dart'; import 'chat_item.dart'; @@ -1066,27 +1067,43 @@ class _ChatForwardWidgetState extends State { widget.me, ); - await ConfirmDialog.show( - context, - title: 'label_delete_message'.l10n, - description: deletable || isMonolog - ? null - : 'label_message_will_deleted_for_you'.l10n, - variants: [ - if (!deletable || !isMonolog) - ConfirmDialogVariant( - key: const Key('HideForMe'), - onProceed: widget.onHide, - label: 'label_delete_for_me'.l10n, + bool deleteForAll = false; + + final bool? pressed = await MessagePopup.alert( + 'label_delete_message'.l10n, + description: [ + if (!deletable && !isMonolog) + TextSpan( + text: + 'label_this_message_will_be_deleted_only_for_you' + .l10n, ), - if (deletable) - ConfirmDialogVariant( - key: const Key('DeleteForAll'), - onProceed: widget.onDelete, - label: 'label_delete_for_everyone'.l10n, + ], + additional: [ + if (deletable && !isMonolog) + StatefulBuilder( + builder: (context, setState) { + return RowCheckboxButton( + key: const Key('DeleteForAll'), + label: 'label_also_delete_for_everyone' + .l10n, + value: deleteForAll, + onPressed: (e) => + setState(() => deleteForAll = e), + ); + }, ), ], + button: MessagePopup.deleteButton, ); + + if (pressed ?? false) { + if (deletable && (isMonolog || deleteForAll)) { + widget.onDelete?.call(); + } else if (!isMonolog) { + widget.onHide?.call(); + } + } }, ), ContextMenuButton( diff --git a/lib/ui/page/home/page/chat/widget/chat_item.dart b/lib/ui/page/home/page/chat/widget/chat_item.dart index 4fdaf44b9ff..30ef8fa758b 100644 --- a/lib/ui/page/home/page/chat/widget/chat_item.dart +++ b/lib/ui/page/home/page/chat/widget/chat_item.dart @@ -47,16 +47,17 @@ import '/themes.dart'; import '/ui/page/call/widget/fit_view.dart'; import '/ui/page/home/page/chat/forward/view.dart'; import '/ui/page/home/widget/avatar.dart'; -import '/ui/page/home/widget/confirm_dialog.dart'; import '/ui/page/home/widget/gallery_popup.dart'; import '/ui/page/home/widget/retry_image.dart'; import '/ui/widget/animations.dart'; +import '/ui/widget/checkbox_button.dart'; import '/ui/widget/context_menu/menu.dart'; import '/ui/widget/context_menu/region.dart'; import '/ui/widget/future_or_builder.dart'; import '/ui/widget/svg/svg.dart'; import '/ui/widget/widget_button.dart'; import '/util/fixed_timer.dart'; +import '/util/message_popup.dart'; import '/util/platform_utils.dart'; import 'animated_offset.dart'; import 'chat_gallery.dart'; @@ -1585,28 +1586,44 @@ class _ChatItemWidgetState extends State { ) && widget.item.value is ChatMessage; - await ConfirmDialog.show( - context, - title: 'label_delete_message'.l10n, - description: deletable || isMonolog - ? null - : 'label_message_will_deleted_for_you'.l10n, - initial: 1, - variants: [ - if (!deletable || !isMonolog) - ConfirmDialogVariant( - key: const Key('HideForMe'), - onProceed: widget.onHide, - label: 'label_delete_for_me'.l10n, + bool deleteForAll = false; + + final bool? pressed = await MessagePopup.alert( + 'label_delete_message'.l10n, + description: [ + if (!deletable && !isMonolog) + TextSpan( + text: + 'label_this_message_will_be_deleted_only_for_you' + .l10n, ), - if (deletable) - ConfirmDialogVariant( - key: const Key('DeleteForAll'), - onProceed: widget.onDelete, - label: 'label_delete_for_everyone'.l10n, + ], + additional: [ + if (deletable && !isMonolog) + StatefulBuilder( + builder: (context, setState) { + return RowCheckboxButton( + key: const Key('DeleteForAll'), + label: + 'label_also_delete_for_everyone' + .l10n, + value: deleteForAll, + onPressed: (e) => + setState(() => deleteForAll = e), + ); + }, ), ], + button: MessagePopup.deleteButton, ); + + if (pressed ?? false) { + if (deletable && (isMonolog || deleteForAll)) { + widget.onDelete?.call(); + } else if (!isMonolog) { + widget.onHide?.call(); + } + } }, ), ], @@ -1628,17 +1645,14 @@ class _ChatItemWidgetState extends State { trailing: const SvgIcon(SvgIcons.delete19), inverted: const SvgIcon(SvgIcons.delete19White), onPressed: () async { - await ConfirmDialog.show( - context, - title: 'label_delete_message'.l10n, - variants: [ - ConfirmDialogVariant( - key: const Key('DeleteForAll'), - onProceed: widget.onDelete, - label: 'label_delete_for_everyone'.l10n, - ), - ], + final bool? pressed = await MessagePopup.alert( + 'label_delete_message'.l10n, + button: MessagePopup.deleteButton, ); + + if (pressed ?? false) { + widget.onDelete?.call(); + } }, ), ], diff --git a/lib/ui/page/home/widget/confirm_dialog.dart b/lib/ui/page/home/widget/confirm_dialog.dart deleted file mode 100644 index 3d4b5fb4312..00000000000 --- a/lib/ui/page/home/widget/confirm_dialog.dart +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright © 2022-2025 IT ENGINEERING MANAGEMENT INC, -// -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU Affero General Public License v3.0 as published by the -// Free Software Foundation, either version 3 of the License, or (at your -// option) any later version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License v3.0 for -// more details. -// -// You should have received a copy of the GNU Affero General Public License v3.0 -// along with this program. If not, see -// . - -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import '/l10n/l10n.dart'; -import '/themes.dart'; -import '/ui/widget/modal_popup.dart'; -import '/ui/widget/primary_button.dart'; -import 'rectangle_button.dart'; - -/// Variant of a [ConfirmDialog]. -class ConfirmDialogVariant { - const ConfirmDialogVariant({this.key, required this.label, this.onProceed}); - - /// [Key] of this [ConfirmDialogVariant]. - final Key? key; - - /// Callback, called when this [ConfirmDialogVariant] is submitted. - final T? Function()? onProceed; - - /// Label representing this [ConfirmDialogVariant]. - final String label; -} - -/// Dialog confirming a specific action from the provided [variants]. -/// -/// Intended to be displayed with the [show] method. -class ConfirmDialog extends StatefulWidget { - ConfirmDialog({ - super.key, - this.description, - required this.title, - required this.variants, - this.initial = 0, - this.label, - this.additional = const [], - }) : assert(variants.isNotEmpty); - - /// [ConfirmDialogVariant]s of this [ConfirmDialog]. - final List variants; - - /// Title of this [ConfirmDialog]. - final String title; - - /// Optional description to display above the [variants]. - final String? description; - - /// Label of the submit button. - final String? label; - - /// [Widget]s to put above the [description]. - final List additional; - - /// Index of the [variants] to be initially selected. - final int initial; - - /// Displays a [ConfirmDialog] wrapped in a [ModalPopup]. - static Future show( - BuildContext context, { - String? description, - required String title, - required List variants, - String? label, - List additional = const [], - int initial = 0, - }) { - return ModalPopup.show( - context: context, - child: ConfirmDialog( - description: description, - title: title, - variants: variants, - additional: additional, - label: label, - initial: initial, - ), - ); - } - - @override - State createState() => _ConfirmDialogState(); -} - -/// State of a [ConfirmDialog] keeping the selected [ConfirmDialogVariant]. -class _ConfirmDialogState extends State { - /// Currently selected [ConfirmDialogVariant]. - late ConfirmDialogVariant _selected; - - /// [ScrollController] to pass to a [Scrollbar]. - final ScrollController _scrollController = ScrollController(); - - @override - void didUpdateWidget(ConfirmDialog oldWidget) { - if (!widget.variants.contains(_selected)) { - setState(() => _selected = widget.variants.first); - } - - super.didUpdateWidget(oldWidget); - } - - @override - void initState() { - _selected = widget - .variants[max(min(widget.initial, widget.variants.length - 1), 0)]; - super.initState(); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final style = Theme.of(context).style; - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ModalPopupHeader(text: widget.title), - const SizedBox(height: 12), - ...widget.additional.map((e) { - return Padding(padding: ModalPopup.padding(context), child: e); - }), - if (widget.additional.isNotEmpty && - (widget.variants.length > 1 || widget.description != null)) - const SizedBox(height: 15), - if (widget.description != null) - Padding( - padding: ModalPopup.padding(context), - child: Center( - child: Text( - widget.description!, - style: style.fonts.normal.regular.secondary, - ), - ), - ), - if (widget.variants.length > 1 && widget.description != null) - const SizedBox(height: 15), - if (widget.variants.length > 1) - Flexible( - child: Scrollbar( - controller: _scrollController, - child: ListView.separated( - controller: _scrollController, - physics: const ClampingScrollPhysics(), - shrinkWrap: true, - itemBuilder: (c, i) { - final ConfirmDialogVariant variant = widget.variants[i]; - - return Padding( - padding: ModalPopup.padding(context), - child: RectangleButton( - key: variant.key, - selected: _selected == variant, - onPressed: _selected == variant - ? null - : () => setState(() => _selected = variant), - label: variant.label, - radio: true, - ), - ); - }, - separatorBuilder: (c, i) => const SizedBox(height: 10), - itemCount: widget.variants.length, - ), - ), - ), - if (widget.variants.length > 1 || widget.description != null) - const SizedBox(height: 25), - Padding( - padding: ModalPopup.padding(context), - child: PrimaryButton( - key: const Key('Proceed'), - title: widget.label ?? 'btn_proceed'.l10n, - onPressed: () { - Navigator.of(context).pop(_selected.onProceed?.call()); - }, - ), - ), - const SizedBox(height: 12), - ], - ); - } -} diff --git a/lib/ui/widget/svg/svgs.dart b/lib/ui/widget/svg/svgs.dart index 21a5b324047..3c06310fda2 100644 --- a/lib/ui/widget/svg/svgs.dart +++ b/lib/ui/widget/svg/svgs.dart @@ -1797,8 +1797,8 @@ class SvgIcons { static const SvgData delete19White = SvgData( 'assets/icons/delete19_white.svg', - width: 19.88, - height: 19, + width: 19, + height: 18, ); static const SvgData download19 = SvgData( diff --git a/lib/util/message_popup.dart b/lib/util/message_popup.dart index 5b2f92ebe45..7b7c749788e 100644 --- a/lib/util/message_popup.dart +++ b/lib/util/message_popup.dart @@ -135,7 +135,7 @@ class MessagePopup { return Stack( children: [ OutlinedRoundedButton( - key: key, + key: key ?? const Key('Proceed'), maxWidth: double.infinity, onPressed: () => Navigator.of(context).pop(true), color: style.colors.danger, diff --git a/test/e2e/features/chat/delete_messages/.feature b/test/e2e/features/chat/delete_messages/.feature index 2ec6a9a83d5..8e6bc73af49 100644 --- a/test/e2e/features/chat/delete_messages/.feature +++ b/test/e2e/features/chat/delete_messages/.feature @@ -41,6 +41,5 @@ Feature: Chat items are deleted correctly When I long press "For hiding" message And I tap `DeleteMessageButton` button - And I tap `HideForMe` button And I tap `Proceed` button Then I wait until "For hiding" message is absent diff --git a/test/e2e/parameters/keys.dart b/test/e2e/parameters/keys.dart index 210c2cea5c5..4d394807a5d 100644 --- a/test/e2e/parameters/keys.dart +++ b/test/e2e/parameters/keys.dart @@ -105,7 +105,6 @@ enum WidgetKey { ForwardField, GalleryPopup, HideChatButton, - HideForMe, HomeView, Interface, IntroductionScrollable,