diff --git a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs
index dd44c92c09..d97fa3e546 100644
--- a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs
+++ b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Components
private NotificationOverlay notificationOverlay = null!;
private ChatOverlay chatOverlay = null!;
private TestMetadataClient metadataClient = null!;
+ private FriendPresenceNotifier notifier = null!;
[SetUp]
public void Setup() => Schedule(() =>
@@ -45,7 +46,11 @@ namespace osu.Game.Tests.Visual.Components
notificationOverlay,
chatOverlay,
metadataClient,
- new FriendPresenceNotifier()
+ notifier = new FriendPresenceNotifier
+ {
+ // Speeds up tests that don't rely on this debounce a little bit.
+ OfflineDebounceTime = 0
+ }
}
};
@@ -127,5 +132,27 @@ namespace osu.Game.Tests.Visual.Components
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
}
+
+ [Test]
+ public void TestOfflineDebounce()
+ {
+ AddStep("set debounce time", () =>
+ {
+ notifier.NotificationDebounceTime = 0;
+ notifier.OfflineDebounceTime = 5000;
+ });
+
+ AddStep("bring friend online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
+ AddUntilStep("online notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
+
+ for (int i = 0; i < 3; i++)
+ {
+ AddStep("bring friend online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
+ AddStep("bring friend offline", () => metadataClient.FriendPresenceUpdated(1, null));
+ }
+
+ AddUntilStep("online notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
+ AddUntilStep("offline notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
+ }
}
}
diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs
index 77d0421354..6a55b4a2a2 100644
--- a/osu.Game/Online/FriendPresenceNotifier.cs
+++ b/osu.Game/Online/FriendPresenceNotifier.cs
@@ -25,6 +25,16 @@ namespace osu.Game.Online
{
public partial class FriendPresenceNotifier : Component
{
+ ///
+ /// Minimum time between subsequent online/offline notifications.
+ ///
+ public double NotificationDebounceTime { get; set; } = 1000;
+
+ ///
+ /// Minimum time after a user has gone offline, before they're added to the offline alert queue.
+ ///
+ public double OfflineDebounceTime { get; set; } = 15000;
+
[Resolved]
private INotificationOverlay notifications { get; set; } = null!;
@@ -42,13 +52,31 @@ namespace osu.Game.Online
private readonly IBindableList friends = new BindableList();
private readonly IBindableDictionary friendPresences = new BindableDictionary();
+ ///
+ /// List of users that will be notified as having come online with the next notification.
+ ///
private readonly HashSet onlineAlertQueue = new HashSet();
+
+ ///
+ /// List of users that will be notified as having gone offline with the next notification.
+ ///
private readonly HashSet offlineAlertQueue = new HashSet();
- private double? nextOnlineAlertTime;
- private double? nextOfflineAlertTime;
+ ///
+ /// List of users that have gone offline, but we're waiting for them to potentially come online again before queueing them for notification.
+ /// For example, if a user is quickly toggling between the "Online" and "Appear Offline" states.
+ ///
+ private readonly HashSet pendingOfflineUsers = new HashSet();
- private const double debounce_time_before_notification = 1000;
+ ///
+ /// The post time for the next online notification.
+ ///
+ private double? nextOnlineAlertTime;
+
+ ///
+ /// The post time for the next offline notification.
+ ///
+ private double? nextOfflineAlertTime;
protected override void LoadComplete()
{
@@ -133,28 +161,55 @@ namespace osu.Game.Online
APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId);
if (friend?.TargetUser is APIUser user)
- markUserOffline(user);
+ markUserOfflineDebounced(user);
}
break;
}
}
+ ///
+ /// Immediately registers a user for the next online notification alert.
+ ///
private void markUserOnline(APIUser user)
{
+ if (pendingOfflineUsers.Remove(user))
+ return;
+
if (!offlineAlertQueue.Remove(user))
{
onlineAlertQueue.Add(user);
- nextOnlineAlertTime ??= Time.Current + debounce_time_before_notification;
+ nextOnlineAlertTime ??= Time.Current + NotificationDebounceTime;
}
}
+ ///
+ /// Waits before adding a user to the next offline notification alert.
+ ///
+ ///
+ private void markUserOfflineDebounced(APIUser user)
+ {
+ pendingOfflineUsers.Add(user);
+
+ Scheduler.AddDelayed(() =>
+ {
+ // Check if the friend has come back online.
+ if (!pendingOfflineUsers.Remove(user))
+ return;
+
+ markUserOffline(user);
+ }, OfflineDebounceTime);
+ }
+
+ ///
+ /// Immediately registers a user for the next offline notification alert.
+ ///
private void markUserOffline(APIUser user)
{
if (!onlineAlertQueue.Remove(user))
{
offlineAlertQueue.Add(user);
- nextOfflineAlertTime ??= Time.Current + debounce_time_before_notification;
+ nextOfflineAlertTime ??= Time.Current + NotificationDebounceTime;
}
}