mirror of
https://github.com/ppy/osu.git
synced 2025-01-30 03:02:54 +08:00
Display notification on friend presence changes
This commit is contained in:
parent
01e9c0f15e
commit
51b62a6d8e
@ -0,0 +1,129 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Online;
|
||||||
|
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.Tests.Visual.Metadata;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Components
|
||||||
|
{
|
||||||
|
public partial class TestSceneFriendPresenceNotifier : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
private ChannelManager channelManager = null!;
|
||||||
|
private NotificationOverlay notificationOverlay = null!;
|
||||||
|
private ChatOverlay chatOverlay = null!;
|
||||||
|
private TestMetadataClient metadataClient = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(ChannelManager), channelManager = new ChannelManager(API)),
|
||||||
|
(typeof(INotificationOverlay), notificationOverlay = new NotificationOverlay()),
|
||||||
|
(typeof(ChatOverlay), chatOverlay = new ChatOverlay()),
|
||||||
|
(typeof(MetadataClient), metadataClient = new TestMetadataClient()),
|
||||||
|
],
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
channelManager,
|
||||||
|
notificationOverlay,
|
||||||
|
chatOverlay,
|
||||||
|
metadataClient,
|
||||||
|
new FriendPresenceNotifier()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 1; i <= 100; i++)
|
||||||
|
((DummyAPIAccess)API).Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNotifications()
|
||||||
|
{
|
||||||
|
AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
AddStep("bring friend 1 offline", () => metadataClient.UserPresenceUpdated(1, null));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleUserNotificationOpensChat()
|
||||||
|
{
|
||||||
|
AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("click notification", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Notification>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("chat overlay opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
AddUntilStep("user channel selected", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo(((DummyAPIAccess)API).Friends[0].TargetUser!.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleUserNotificationDoesNotOpenChat()
|
||||||
|
{
|
||||||
|
AddStep("bring friends 1 & 2 online", () =>
|
||||||
|
{
|
||||||
|
metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online });
|
||||||
|
metadataClient.UserPresenceUpdated(2, new UserPresence { Status = UserStatus.Online });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("click notification", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Notification>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("chat overlay not opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNonFriendsDoNotNotify()
|
||||||
|
{
|
||||||
|
AddStep("bring non-friend 1000 online", () => metadataClient.UserPresenceUpdated(1000, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddWaitStep("wait for possible notification", 10);
|
||||||
|
AddAssert("no notification", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPostManyDebounced()
|
||||||
|
{
|
||||||
|
AddStep("bring friends 1-10 online", () =>
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 10; i++)
|
||||||
|
metadataClient.UserPresenceUpdated(i, new UserPresence { Status = UserStatus.Online });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("bring friends 1-10 offline", () =>
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 10; i++)
|
||||||
|
metadataClient.UserPresenceUpdated(i, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -75,6 +75,7 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||||
|
|
||||||
|
private readonly Dictionary<int, APIRelation> friendsMapping = new Dictionary<int, APIRelation>();
|
||||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||||
|
|
||||||
private readonly Logger log;
|
private readonly Logger log;
|
||||||
@ -403,6 +404,8 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public IChatClient GetChatClient() => new WebSocketChatClient(this);
|
public IChatClient GetChatClient() => new WebSocketChatClient(this);
|
||||||
|
|
||||||
|
public APIRelation GetFriend(int userId) => friendsMapping.GetValueOrDefault(userId);
|
||||||
|
|
||||||
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
||||||
{
|
{
|
||||||
Debug.Assert(State.Value == APIState.Offline);
|
Debug.Assert(State.Value == APIState.Offline);
|
||||||
@ -594,6 +597,8 @@ namespace osu.Game.Online.API
|
|||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
setLocalUser(createGuestUser());
|
setLocalUser(createGuestUser());
|
||||||
|
|
||||||
|
friendsMapping.Clear();
|
||||||
friends.Clear();
|
friends.Clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -610,7 +615,11 @@ namespace osu.Game.Online.API
|
|||||||
friendsReq.Failure += _ => state.Value = APIState.Failing;
|
friendsReq.Failure += _ => state.Value = APIState.Failing;
|
||||||
friendsReq.Success += res =>
|
friendsReq.Success += res =>
|
||||||
{
|
{
|
||||||
|
friendsMapping.Clear();
|
||||||
friends.Clear();
|
friends.Clear();
|
||||||
|
|
||||||
|
foreach (var u in res)
|
||||||
|
friendsMapping[u.TargetID] = u;
|
||||||
friends.AddRange(res);
|
friends.AddRange(res);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -194,6 +195,8 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public IChatClient GetChatClient() => new TestChatClientConnector(this);
|
public IChatClient GetChatClient() => new TestChatClientConnector(this);
|
||||||
|
|
||||||
|
public APIRelation? GetFriend(int userId) => Friends.FirstOrDefault(r => r.TargetID == userId);
|
||||||
|
|
||||||
public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password)
|
public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password)
|
||||||
{
|
{
|
||||||
Thread.Sleep(200);
|
Thread.Sleep(200);
|
||||||
|
@ -152,6 +152,13 @@ namespace osu.Game.Online.API
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
IChatClient GetChatClient();
|
IChatClient GetChatClient();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a friend from a given user ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The friend's user ID.</param>
|
||||||
|
/// <returns>The <see cref="APIRelation"/> object representing the friend, if any.</returns>
|
||||||
|
APIRelation? GetFriend(int userId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new user account. This is a blocking operation.
|
/// Create a new user account. This is a blocking operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
148
osu.Game/Online/FriendPresenceNotifier.cs
Normal file
148
osu.Game/Online/FriendPresenceNotifier.cs
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
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!;
|
||||||
|
|
||||||
|
private readonly IBindableDictionary<int, UserPresence> userStates = 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();
|
||||||
|
|
||||||
|
userStates.BindTo(metadataClient.UserStates);
|
||||||
|
userStates.BindCollectionChanged((_, args) =>
|
||||||
|
{
|
||||||
|
switch (args.Action)
|
||||||
|
{
|
||||||
|
case NotifyDictionaryChangedAction.Add:
|
||||||
|
foreach ((int userId, var _) in args.NewItems!)
|
||||||
|
{
|
||||||
|
if (api.GetFriend(userId)?.TargetUser is APIUser user)
|
||||||
|
{
|
||||||
|
if (!offlineAlertQueue.Remove(user))
|
||||||
|
{
|
||||||
|
onlineAlertQueue.Add(user);
|
||||||
|
lastOnlineAlertTime ??= Time.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyDictionaryChangedAction.Remove:
|
||||||
|
foreach ((int userId, var _) in args.OldItems!)
|
||||||
|
{
|
||||||
|
if (api.GetFriend(userId)?.TargetUser is APIUser user)
|
||||||
|
{
|
||||||
|
if (!onlineAlertQueue.Remove(user))
|
||||||
|
{
|
||||||
|
offlineAlertQueue.Add(user);
|
||||||
|
lastOfflineAlertTime ??= Time.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
alertOnlineUsers();
|
||||||
|
alertOfflineUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void alertOnlineUsers()
|
||||||
|
{
|
||||||
|
if (onlineAlertQueue.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000)
|
||||||
|
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;
|
||||||
|
|
||||||
|
notifications.Post(new SimpleNotification
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.UserMinus,
|
||||||
|
Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}",
|
||||||
|
IconColour = colours.Red
|
||||||
|
});
|
||||||
|
|
||||||
|
offlineAlertQueue.Clear();
|
||||||
|
lastOfflineAlertTime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1151,6 +1151,7 @@ namespace osu.Game
|
|||||||
Add(externalLinkOpener = new ExternalLinkOpener());
|
Add(externalLinkOpener = new ExternalLinkOpener());
|
||||||
Add(new MusicKeyBindingHandler());
|
Add(new MusicKeyBindingHandler());
|
||||||
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));
|
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));
|
||||||
|
Add(new FriendPresenceNotifier());
|
||||||
|
|
||||||
// side overlays which cancel each other.
|
// side overlays which cancel each other.
|
||||||
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay };
|
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay };
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -66,7 +67,7 @@ namespace osu.Game.Tests.Visual.Metadata
|
|||||||
|
|
||||||
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
|
public override Task UserPresenceUpdated(int userId, UserPresence? presence)
|
||||||
{
|
{
|
||||||
if (isWatchingUserPresence.Value)
|
if (isWatchingUserPresence.Value || api.Friends.Any(f => f.TargetID == userId))
|
||||||
{
|
{
|
||||||
if (presence.HasValue)
|
if (presence.HasValue)
|
||||||
userStates[userId] = presence.Value;
|
userStates[userId] = presence.Value;
|
||||||
|
Loading…
Reference in New Issue
Block a user