1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 08:30:02 +08:00
Files
osu-lazer/osu.Game/Online/FriendPresenceNotifier.cs
T
SupDos 5cdf07c7e1 Make grouped friend notifications Transient and not important (#36620)
Grouped notifications for more than 1 person were added in
https://github.com/ppy/osu/pull/36180 but it looks like they forgot to
add the Transient and IsImportant flags, which means the grouped
notifications would still stay in the notification list/flash the
taskbar.

Before:


https://github.com/user-attachments/assets/8a34bbc0-2b5c-4086-b2ee-1daa6d1e6e10



After:


https://github.com/user-attachments/assets/03c25ba6-7c8e-464c-bbb1-688ab9da6bb6
2026-02-08 23:16:39 +09:00

279 lines
9.7 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Metadata;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Users;
using osuTK.Graphics;
namespace osu.Game.Online
{
public partial class FriendPresenceNotifier : Component
{
[Resolved]
private INotificationOverlay notifications { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private MetadataClient metadataClient { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private readonly Bindable<bool> notifyOnFriendPresenceChange = new BindableBool();
private readonly IBindableList<APIRelation> friends = new BindableList<APIRelation>();
private readonly IBindableDictionary<int, UserPresence> friendPresences = new BindableDictionary<int, UserPresence>();
private readonly HashSet<APIUser> onlineAlertQueue = new HashSet<APIUser>();
private readonly HashSet<APIUser> offlineAlertQueue = new HashSet<APIUser>();
private double? nextOnlineAlertTime;
private double? nextOfflineAlertTime;
private const double debounce_time_before_notification = 1000;
protected override void LoadComplete()
{
base.LoadComplete();
config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange);
notifyOnFriendPresenceChange.BindValueChanged(_ =>
{
onlineAlertQueue.Clear();
offlineAlertQueue.Clear();
nextOfflineAlertTime = null;
nextOnlineAlertTime = null;
});
friends.BindTo(api.LocalUserState.Friends);
friends.BindCollectionChanged(onFriendsChanged, true);
friendPresences.BindTo(metadataClient.FriendPresences);
friendPresences.BindCollectionChanged(onFriendPresenceChanged, true);
}
protected override void Update()
{
base.Update();
if (notifyOnFriendPresenceChange.Value)
{
alertOnlineUsers();
alertOfflineUsers();
}
}
private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (APIRelation friend in e.NewItems!.Cast<APIRelation>())
{
if (friend.TargetUser is not APIUser user)
continue;
if (friendPresences.TryGetValue(friend.TargetID, out _))
markUserOnline(user);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (APIRelation friend in e.OldItems!.Cast<APIRelation>())
{
if (friend.TargetUser is not APIUser user)
continue;
onlineAlertQueue.Remove(user);
offlineAlertQueue.Remove(user);
}
break;
}
}
private void onFriendPresenceChanged(object? sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e)
{
switch (e.Action)
{
case NotifyDictionaryChangedAction.Add:
foreach ((int friendId, _) in e.NewItems!)
{
APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId);
if (friend?.TargetUser is APIUser user)
markUserOnline(user);
}
break;
case NotifyDictionaryChangedAction.Remove:
foreach ((int friendId, _) in e.OldItems!)
{
APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId);
if (friend?.TargetUser is APIUser user)
markUserOffline(user);
}
break;
}
}
private void markUserOnline(APIUser user)
{
if (!offlineAlertQueue.Remove(user))
{
onlineAlertQueue.Add(user);
nextOnlineAlertTime ??= Time.Current + debounce_time_before_notification;
}
}
private void markUserOffline(APIUser user)
{
if (!onlineAlertQueue.Remove(user))
{
offlineAlertQueue.Add(user);
nextOfflineAlertTime ??= Time.Current + debounce_time_before_notification;
}
}
private void alertOnlineUsers()
{
if (nextOnlineAlertTime == null || Time.Current < nextOnlineAlertTime)
return;
// If a user quickly switches online-offline, we might reach here without actually having a notification
// to fire. Importantly, we should still reset the next alert time in such a scenario.
if (onlineAlertQueue.Count == 1)
notifications.Post(new SingleFriendOnlineNotification(onlineAlertQueue.Single()));
else if (onlineAlertQueue.Count > 1)
notifications.Post(new MultipleFriendsOnlineNotification(onlineAlertQueue.ToArray()));
onlineAlertQueue.Clear();
nextOnlineAlertTime = null;
}
private void alertOfflineUsers()
{
if (nextOfflineAlertTime == null || Time.Current < nextOfflineAlertTime)
return;
// If a user quickly switches offline-online, we might reach here without actually having a notification
// to fire. Importantly, we should still reset the next alert time in such a scenario.
if (offlineAlertQueue.Count == 1)
notifications.Post(new SingleFriendOfflineNotification(offlineAlertQueue.Single()));
else if (offlineAlertQueue.Count > 1)
notifications.Post(new MultipleFriendsOfflineNotification(offlineAlertQueue.ToArray()));
offlineAlertQueue.Clear();
nextOfflineAlertTime = null;
}
private partial class SingleFriendOnlineNotification : UserAvatarNotification
{
public SingleFriendOnlineNotification(APIUser user)
: base(user)
{
Transient = true;
IsImportant = false;
Text = NotificationsStrings.FriendOnline(User.Username);
}
[BackgroundDependencyLoader]
private void load(ChannelManager channelManager, ChatOverlay chatOverlay)
{
Activated = () =>
{
channelManager.OpenPrivateChannel(User);
chatOverlay.Show();
return true;
};
}
public override string PopInSampleName => "UI/notification-friend-online";
}
private partial class MultipleFriendsOnlineNotification : SimpleNotification
{
public MultipleFriendsOnlineNotification(ICollection<APIUser> users)
{
Transient = true;
IsImportant = false;
Text = NotificationsStrings.FriendOnline(string.Join(@", ", users.Select(u => u.Username)));
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.User;
IconColour = colours.Green;
}
public override string PopInSampleName => "UI/notification-friend-online";
}
private partial class SingleFriendOfflineNotification : UserAvatarNotification
{
public SingleFriendOfflineNotification(APIUser user)
: base(user)
{
Transient = true;
IsImportant = false;
Text = NotificationsStrings.FriendOffline(User.Username);
}
[BackgroundDependencyLoader]
private void load()
{
Icon = FontAwesome.Solid.UserSlash;
Avatar.Colour = Color4.White.Opacity(0.25f);
}
public override string PopInSampleName => "UI/notification-friend-offline";
}
private partial class MultipleFriendsOfflineNotification : SimpleNotification
{
public MultipleFriendsOfflineNotification(ICollection<APIUser> users)
{
Transient = true;
IsImportant = false;
Text = NotificationsStrings.FriendOffline(string.Join(@", ", users.Select(u => u.Username)));
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.UserSlash;
IconColour = colours.Red;
}
public override string PopInSampleName => "UI/notification-friend-offline";
}
}
}