1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 14:52:56 +08:00
osu-lazer/osu.Game/Online/FriendPresenceNotifier.cs
Dan Balasescu 7268b2e077
Add separate path for friend presence notifications
It proved to be too difficult to deal with the flow that clears user
states on stopping the watching of global presence updates. It's not
helped in the least that friends are updated via the API, so there's a
third flow to consider (and the timings therein - both server-spectator
and friends are updated concurrently).

Simplest is to separate the friends flow, though this does mean some
logic and state duplication.
2025-01-09 17:31:01 +09:00

217 lines
6.8 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.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
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;
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 ChannelManager channelManager { get; set; } = null!;
[Resolved]
private ChatOverlay chatOverlay { get; set; } = null!;
[Resolved]
private OsuColour colours { 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> friendStates = new BindableDictionary<int, UserPresence>();
private readonly HashSet<APIUser> onlineAlertQueue = new HashSet<APIUser>();
private readonly HashSet<APIUser> offlineAlertQueue = new HashSet<APIUser>();
private double? lastOnlineAlertTime;
private double? lastOfflineAlertTime;
protected override void LoadComplete()
{
base.LoadComplete();
config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange);
friends.BindTo(api.Friends);
friends.BindCollectionChanged(onFriendsChanged, true);
friendStates.BindTo(metadataClient.FriendStates);
friendStates.BindCollectionChanged(onFriendStatesChanged, true);
}
protected override void Update()
{
base.Update();
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 (friendStates.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 onFriendStatesChanged(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);
lastOnlineAlertTime ??= Time.Current;
}
}
private void markUserOffline(APIUser user)
{
if (!onlineAlertQueue.Remove(user))
{
offlineAlertQueue.Add(user);
lastOfflineAlertTime ??= Time.Current;
}
}
private void alertOnlineUsers()
{
if (onlineAlertQueue.Count == 0)
return;
if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000)
return;
if (!notifyOnFriendPresenceChange.Value)
{
lastOnlineAlertTime = null;
return;
}
APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null;
notifications.Post(new SimpleNotification
{
Icon = FontAwesome.Solid.UserPlus,
Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}",
IconColour = colours.Green,
Activated = () =>
{
if (singleUser != null)
{
channelManager.OpenPrivateChannel(singleUser);
chatOverlay.Show();
}
return true;
}
});
onlineAlertQueue.Clear();
lastOnlineAlertTime = null;
}
private void alertOfflineUsers()
{
if (offlineAlertQueue.Count == 0)
return;
if (lastOfflineAlertTime == null || Time.Current - lastOfflineAlertTime < 1000)
return;
if (!notifyOnFriendPresenceChange.Value)
{
lastOfflineAlertTime = null;
return;
}
notifications.Post(new SimpleNotification
{
Icon = FontAwesome.Solid.UserMinus,
Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}",
IconColour = colours.Red
});
offlineAlertQueue.Clear();
lastOfflineAlertTime = null;
}
}
}