blob: dd10c61354f448aaa306aa799e31fc5eab9556b6 [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'config_specific/launch_url/launch_url.dart';
import 'globals.dart';
import 'primitives/utils.dart';
class NotificationMessage {
NotificationMessage(
this.text, {
this.actions = const [],
this.duration = defaultDuration,
this.isError = false,
this.isDismissible = false,
});
/// The default duration for notifications to show.
static const Duration defaultDuration = Duration(seconds: 7);
final String text;
final List<Widget> actions;
final Duration duration;
final bool isError;
final bool isDismissible;
}
/// Collects tasks to show or dismiss notifications in UI.
class NotificationService {
final toPush = Queue<NotificationMessage>();
final toDismiss = Queue<NotificationMessage>();
/// Notifies about added messages or dismissals.
final ValueNotifier<int> newTasks = ValueNotifier(0);
/// Messages that are planned to be shown or are currently shown in UI.
@visibleForTesting
final activeMessages = <NotificationMessage>[];
/// Pushes a notification [message].
///
/// Includes a button to close the notification if [isDismissible] is true,
/// otherwise the notification will be automatically dismissed after
/// [NotificationMessage.defaultDuration].
bool push(
String message, {
isDismissible = false,
}) =>
pushNotification(
NotificationMessage(
message,
isDismissible: isDismissible,
),
);
/// Pushes an error notification with [errorMessage] as the text.
///
/// Includes an action to report the error by opening the link to our issue
/// tracker if [isReportable] is true. Includes a button to close the error if
/// [isDismissible] is true, otherwise the error will be automatically
/// dismissed after [NotificationMessage.defaultDuration].
bool pushError(
String errorMessage, {
isDismissible = true,
isReportable = true,
}) {
final reportErrorAction = NotificationAction(
'Report error',
() {
unawaited(
launchUrl(
devToolsExtensionPoints
.issueTrackerLink(
issueTitle: 'Reporting error: $errorMessage',
)
.url,
),
);
},
);
return pushNotification(
NotificationMessage(
errorMessage,
isError: true,
isDismissible: isDismissible,
actions: [if (isReportable) reportErrorAction],
// Double the duration so that the user has time to report the error:
duration: isReportable
? NotificationMessage.defaultDuration * 2
: NotificationMessage.defaultDuration,
),
allowDuplicates: false,
);
}
/// Pushes a notification [message].
///
/// Ignores the message if [allowDuplicates] is false and a message with the
/// same text is currently displayed to the user.
bool pushNotification(
NotificationMessage message, {
bool allowDuplicates = true,
}) {
if (!allowDuplicates &&
activeMessages.containsWhere((m) => m.text == message.text)) {
return false;
}
activeMessages.add(message);
toPush.add(message);
newTasks.value++;
return true;
}
/// Dismisses all notifications with a matching message.
void dismiss(String message) {
// Remove those that were not picked up yet by UI.
final toRemove = toPush.where((e) => e.text == message).toList();
for (var messageToRemove in toRemove) {
toPush.remove(messageToRemove);
activeMessages.remove(messageToRemove);
}
// Add task to dismiss for those that were picked up by UI.
if (activeMessages.containsWhere((element) => element.text == message)) {
toDismiss.addLast(NotificationMessage(message));
newTasks.value++;
}
}
/// Marks the message as complete, so that the messages not
/// allowing duplicates,
/// with the same text, do not get rejected.
void markComplete(NotificationMessage message) {
activeMessages.removeWhere((element) => element == message);
}
void dispose() {
newTasks.dispose();
}
}
class NotificationAction extends StatelessWidget {
const NotificationAction(
this.label,
this.onAction, {
super.key,
this.isPrimary = false,
});
final String label;
final VoidCallback onAction;
final bool isPrimary;
@override
Widget build(BuildContext context) {
final labelText = Text(label);
return isPrimary
? ElevatedButton(
onPressed: onAction,
child: labelText,
)
: OutlinedButton(
onPressed: onAction,
child: labelText,
);
}
}