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,