From 5eda9a0fd7bbfef8eb1bfdded914b5c2b3de736e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Oct 2025 10:53:58 +0200 Subject: [PATCH] Extract all pieces of local user-related state to `APIAccess` subcomponent Something I've asked to be done for a long time. Relevant because I've complained about this on every addition of a new piece of user-local state: friends, blocks, and now favourite beatmaps. It's just so messy managing all this inside `APIAccess` next to everything else, IMO. --- .../TestSceneFriendPresenceNotifier.cs | 6 +- .../Gameplay/TestSceneGameplayLeaderboard.cs | 8 +- .../Online/TestSceneDashboardOverlay.cs | 2 +- .../Visual/Online/TestSceneFriendDisplay.cs | 26 ++-- .../Online/TestSceneUserProfileHeader.cs | 12 +- .../TestSceneFriendsOnlineStatusControl.cs | 12 +- osu.Game/Online/API/APIAccess.cs | 122 ++--------------- osu.Game/Online/API/DummyAPIAccess.cs | 39 ++++-- osu.Game/Online/API/IAPIProvider.cs | 21 +-- osu.Game/Online/API/ILocalUserState.cs | 18 +++ osu.Game/Online/API/LocalUserState.cs | 128 ++++++++++++++++++ osu.Game/Online/FriendPresenceNotifier.cs | 2 +- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- .../Overlays/Chat/DrawableChatUsername.cs | 2 +- .../Dashboard/Friends/FriendDisplay.cs | 2 +- .../Friends/FriendOnlineStreamControl.cs | 2 +- .../Header/Components/FollowersButton.cs | 4 +- .../Header/Components/UserActionsButton.cs | 2 +- .../DailyChallengeLeaderboard.cs | 2 +- .../Matchmaking/Match/PlayerPanel.cs | 2 +- .../HUD/DrawableGameplayLeaderboardScore.cs | 2 +- .../SelectV2/BeatmapLeaderboardWedge.cs | 2 +- osu.Game/Users/ConfirmBlockActionDialog.cs | 2 +- osu.Game/Users/UserPanel.cs | 2 +- 24 files changed, 239 insertions(+), 183 deletions(-) create mode 100644 osu.Game/Online/API/ILocalUserState.cs create mode 100644 osu.Game/Online/API/LocalUserState.cs diff --git a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs index 2fe2326508..dd44c92c09 100644 --- a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs +++ b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Components }; for (int i = 1; i <= 100; i++) - ((DummyAPIAccess)API).Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } }); + ((DummyAPIAccess)API).LocalUserState.Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } }); }); [Test] @@ -75,7 +75,9 @@ namespace osu.Game.Tests.Visual.Components }); 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)); + AddUntilStep("user channel selected", + () => channelManager.CurrentChannel.Value.Name, + () => Is.EqualTo(((DummyAPIAccess)API).LocalUserState.Friends[0].TargetUser!.Username)); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index a54c40014a..e1103dcb92 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -90,8 +90,8 @@ namespace osu.Game.Tests.Visual.Gameplay var api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.Add(new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.Add(new APIRelation { Mutual = true, RelationType = RelationType.Friend, @@ -129,8 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay var api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.Add(new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.Add(new APIRelation { Mutual = true, RelationType = RelationType.Friend, diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs index 13b7e6e18c..1c946cfef9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Online if (supportLevel > 3) supportLevel = 0; - ((DummyAPIAccess)API).Friends.Add(new APIRelation + ((DummyAPIAccess)API).LocalUserState.Friends.Add(new APIRelation { TargetID = 2, RelationType = RelationType.Friend, diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 805ac44829..b9c1478fed 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -59,8 +59,8 @@ namespace osu.Game.Tests.Visual.Online AddStep("set friends", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.AddRange(getUsers().Select(u => new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation { RelationType = RelationType.Friend, TargetID = u.OnlineID, @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("remove one friend", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.RemoveAt(0); + api.LocalUserState.Friends.RemoveAt(0); }); waitForLoad(); @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("add one friend", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.AddRange(getUsers().Take(1).Select(u => new APIRelation + api.LocalUserState.Friends.AddRange(getUsers().Take(1).Select(u => new APIRelation { RelationType = RelationType.Friend, TargetID = u.OnlineID, @@ -101,8 +101,8 @@ namespace osu.Game.Tests.Visual.Online AddStep("set friends", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.AddRange(getUsers().Select(u => new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation { RelationType = RelationType.Friend, TargetID = u.OnlineID, @@ -130,8 +130,8 @@ namespace osu.Game.Tests.Visual.Online AddStep("set friends", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.AddRange(getUsers().Select(u => new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation { RelationType = RelationType.Friend, TargetID = u.OnlineID, @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("bring a friend online", () => { DummyAPIAccess api = (DummyAPIAccess)API; - metadataClient.FriendPresenceUpdated(api.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(api.LocalUserState.Friends[0].TargetID, new UserPresence { Status = UserStatus.Online }); }); assertVisiblePanelCount(1); @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("bring a friend online", () => { DummyAPIAccess api = (DummyAPIAccess)API; - metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(api.LocalUserState.Friends[1].TargetID, new UserPresence { Status = UserStatus.Online }); }); assertVisiblePanelCount(1); @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("take friend offline", () => { DummyAPIAccess api = (DummyAPIAccess)API; - metadataClient.FriendPresenceUpdated(api.Friends[1].TargetID, null); + metadataClient.FriendPresenceUpdated(api.LocalUserState.Friends[1].TargetID, null); }); assertVisiblePanelCount(1); @@ -184,8 +184,8 @@ namespace osu.Game.Tests.Visual.Online AddStep("set friends", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.AddRange(getUsers().Select(u => new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.AddRange(getUsers().Select(u => new APIRelation { RelationType = RelationType.Friend, TargetID = u.OnlineID, diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index d3be8d3b98..adfe95a41c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -443,7 +443,7 @@ namespace osu.Game.Tests.Visual.Online Task.Run(() => { requestLock.Wait(3000); - dummyAPI.Friends.Add(apiRelation); + dummyAPI.LocalUserState.Friends.Add(apiRelation); req.TriggerSuccess(new AddFriendResponse { UserRelation = apiRelation @@ -453,11 +453,11 @@ namespace osu.Game.Tests.Visual.Online return true; }; }); - AddStep("clear friend list", () => dummyAPI.Friends.Clear()); + AddStep("clear friend list", () => dummyAPI.LocalUserState.Friends.Clear()); AddStep("Show non-friend user", () => header.User.Value = new UserProfileData(nonFriend, new OsuRuleset().RulesetInfo)); AddStep("Click followers button", () => this.ChildrenOfType().First().TriggerClick()); AddStep("Complete request", () => requestLock.Set()); - AddUntilStep("Friend added", () => API.Friends.Any(f => f.TargetID == nonFriend.OnlineID)); + AddUntilStep("Friend added", () => API.LocalUserState.Friends.Any(f => f.TargetID == nonFriend.OnlineID)); } [Test] @@ -486,7 +486,7 @@ namespace osu.Game.Tests.Visual.Online Task.Run(() => { requestLock.Wait(3000); - dummyAPI.Friends.Add(apiRelation); + dummyAPI.LocalUserState.Friends.Add(apiRelation); req.TriggerSuccess(new AddFriendResponse { UserRelation = apiRelation @@ -496,11 +496,11 @@ namespace osu.Game.Tests.Visual.Online return true; }; }); - AddStep("clear friend list", () => dummyAPI.Friends.Clear()); + AddStep("clear friend list", () => dummyAPI.LocalUserState.Friends.Clear()); AddStep("Show non-friend user", () => header.User.Value = new UserProfileData(nonFriend, new OsuRuleset().RulesetInfo)); AddStep("Click followers button", () => this.ChildrenOfType().First().TriggerClick()); AddStep("Complete request", () => requestLock.Set()); - AddUntilStep("Friend added", () => API.Friends.Any(f => f.TargetID == nonFriend.OnlineID)); + AddUntilStep("Friend added", () => API.LocalUserState.Friends.Any(f => f.TargetID == nonFriend.OnlineID)); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index c7e2a0ed4b..899e6077cd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -50,8 +50,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set 10 friends", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation { RelationType = RelationType.Friend, TargetID = i, @@ -62,8 +62,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set 20 friends", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.AddRange(Enumerable.Range(1, 20).Select(i => new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.AddRange(Enumerable.Range(1, 20).Select(i => new APIRelation { RelationType = RelationType.Friend, TargetID = i, @@ -78,8 +78,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set 10 friends", () => { DummyAPIAccess api = (DummyAPIAccess)API; - api.Friends.Clear(); - api.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation + api.LocalUserState.Friends.Clear(); + api.LocalUserState.Friends.AddRange(Enumerable.Range(1, 10).Select(i => new APIRelation { RelationType = RelationType.Friend, TargetID = i, diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 58171a2f8a..6694003b31 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Sockets; @@ -18,7 +17,7 @@ using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Localisation; @@ -26,11 +25,10 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications.WebSocket; -using osu.Game.Users; namespace osu.Game.Online.API { - public partial class APIAccess : Component, IAPIProvider + public partial class APIAccess : CompositeComponent, IAPIProvider { private readonly OsuGameBase game; private readonly OsuConfigManager config; @@ -53,30 +51,23 @@ namespace osu.Game.Online.API public string ProvidedUsername { get; private set; } - public SessionVerificationMethod? SessionVerificationMethod { get; set; } + public SessionVerificationMethod? SessionVerificationMethod { get; private set; } public string SecondFactorCode { get; private set; } private string password; - public IBindable LocalUser => localUser; - public IBindableList Friends => friends; - public IBindableList Blocks => blocks; + public IBindable LocalUser => localUserState.User; + + public ILocalUserState LocalUserState => localUserState; + private readonly LocalUserState localUserState; public INotificationsClient NotificationsClient { get; } public Language Language => game.CurrentLanguage.Value; - private Bindable localUser { get; } = new Bindable(createGuestUser()); - - private BindableList friends { get; } = new BindableList(); - private BindableList blocks { get; } = new BindableList(); - protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); - private readonly Bindable configStatus = new Bindable(); - private readonly Bindable configSupporter = new Bindable(); - private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; @@ -108,13 +99,12 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - config.BindWith(OsuSetting.UserOnlineStatus, configStatus); - config.BindWith(OsuSetting.WasSupporter, configSupporter); + AddInternal(localUserState = new LocalUserState(this, config)); if (HasLogin) { // Early call to ensure the local user / "logged in" state is correct immediately. - setPlaceholderLocalUser(); + localUserState.SetPlaceholderLocalUser(ProvidedUsername); // This is required so that Queue() requests during startup sequence don't fail due to "not logged in". state.Value = APIState.Connecting; @@ -249,8 +239,8 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - if (localUser.IsDefault) - Scheduler.Add(setPlaceholderLocalUser, false); + if (LocalUser.IsDefault) + Scheduler.Add(localUserState.SetPlaceholderLocalUser, ProvidedUsername, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -348,8 +338,7 @@ namespace osu.Game.Online.API { Debug.Assert(ThreadSafety.IsUpdateThread); - localUser.Value = me; - configSupporter.Value = me.IsSupporter; + localUserState.SetLocalUser(me); SessionVerificationMethod = me.SessionVerificationMethod; state.Value = SessionVerificationMethod == null ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; @@ -365,8 +354,6 @@ namespace osu.Game.Online.API } } - UpdateLocalFriends(); - // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests // before actually going online. @@ -374,23 +361,6 @@ namespace osu.Game.Online.API Thread.Sleep(500); } - /// - /// Show a placeholder user if saved credentials are available. - /// This is useful for storing local scores and showing a placeholder username after starting the game, - /// until a valid connection has been established. - /// - private void setPlaceholderLocalUser() - { - if (!localUser.IsDefault) - return; - - localUser.Value = new APIUser - { - Username = ProvidedUsername, - IsSupporter = configSupporter.Value, - }; - } - public void Perform(APIRequest request) { try @@ -619,78 +589,12 @@ namespace osu.Game.Online.API SecondFactorCode = null; authentication.Clear(); - // Reset the status to be broadcast on the next login, in case multiple players share the same system. - configStatus.Value = UserStatus.Online; - - // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present - Schedule(() => - { - localUser.Value = createGuestUser(); - configSupporter.Value = false; - friends.Clear(); - }); + localUserState.ClearLocalUser(); state.Value = APIState.Offline; flushQueue(); } - public void UpdateLocalFriends() - { - if (!IsLoggedIn) - return; - - var friendsReq = new GetFriendsRequest(); - friendsReq.Failure += ex => - { - if (ex is not WebRequestFlushedException) - state.Value = APIState.Failing; - }; - friendsReq.Success += res => - { - var existingFriends = friends.Select(f => f.TargetID).ToHashSet(); - var updatedFriends = res.Select(f => f.TargetID).ToHashSet(); - - // Add new friends into local list. - friends.AddRange(res.Where(r => !existingFriends.Contains(r.TargetID))); - - // Remove non-friends from local list. - friends.RemoveAll(f => !updatedFriends.Contains(f.TargetID)); - }; - - Queue(friendsReq); - } - - public void UpdateLocalBlocks() - { - if (!IsLoggedIn) - return; - - var blocksReq = new GetBlocksRequest(); - blocksReq.Failure += ex => - { - if (ex is not WebRequestFlushedException) - state.Value = APIState.Failing; - }; - blocksReq.Success += res => - { - var existingBlocks = blocks.Select(f => f.TargetID).ToHashSet(); - var updatedBlocks = res.Select(f => f.TargetID).ToHashSet(); - - // Add new blocked users to local list. - blocks.AddRange(res.Where(r => !existingBlocks.Contains(r.TargetID))); - - // Remove non-blocked users from local list. - blocks.RemoveAll(b => !updatedBlocks.Contains(b.TargetID)); - - // Remove friends who got blocked since last check. - friends.RemoveAll(f => updatedBlocks.Contains(f.TargetID)); - }; - - Queue(blocksReq); - } - - private static APIUser createGuestUser() => new GuestUser(); - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 9750fccb74..dbf5964416 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -20,14 +20,11 @@ namespace osu.Game.Online.API { public const int DUMMY_USER_ID = 1001; - public Bindable LocalUser { get; } = new Bindable(new APIUser - { - Username = @"Local user", - Id = DUMMY_USER_ID, - }); + public DummyLocalUserState LocalUserState { get; } = new DummyLocalUserState(); + public Bindable LocalUser => LocalUserState.User; - public BindableList Friends { get; } = new BindableList(); - public BindableList Blocks { get; } = new BindableList(); + ILocalUserState IAPIProvider.LocalUserState => LocalUserState; + IBindable IAPIProvider.LocalUser => LocalUser; public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -208,10 +205,6 @@ namespace osu.Game.Online.API public void SetState(APIState newState) => state.Value = newState; - IBindable IAPIProvider.LocalUser => LocalUser; - IBindableList IAPIProvider.Friends => Friends; - IBindableList IAPIProvider.Blocks => Blocks; - /// /// Skip 2FA requirement for next login. /// @@ -234,5 +227,29 @@ namespace osu.Game.Online.API // Ensure (as much as we can) that any pending tasks are run. Scheduler.Update(); } + + public class DummyLocalUserState : ILocalUserState + { + public Bindable User { get; } = new Bindable(new APIUser + { + Username = @"Local user", + Id = DUMMY_USER_ID, + }); + + public BindableList Friends { get; } = new BindableList(); + public BindableList Blocks { get; } = new BindableList(); + + IBindable ILocalUserState.User => User; + IBindableList ILocalUserState.Friends => Friends; + IBindableList ILocalUserState.Blocks => Blocks; + + public void UpdateFriends() + { + } + + public void UpdateBlocks() + { + } + } } } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index f3ced9b1ce..de1635fa80 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -19,14 +19,11 @@ namespace osu.Game.Online.API IBindable LocalUser { get; } /// - /// The user's friends. + /// The local user's current state. + /// Contains auxiliary information such as the user's friends, blocks, and favourites, + /// as well as methods to manage those in a way that keeps this state consistent throughout the game. /// - IBindableList Friends { get; } - - /// - /// The users blocked by the local user. - /// - IBindableList Blocks { get; } + ILocalUserState LocalUserState { get; } /// /// The language supplied by this provider to API requests. @@ -123,16 +120,6 @@ namespace osu.Game.Online.API /// void Logout(); - /// - /// Update the friends status of the current user. - /// - void UpdateLocalFriends(); - - /// - /// Update the list of users blocked by the current user. - /// - void UpdateLocalBlocks(); - /// /// Schedule a callback to run on the update thread. /// diff --git a/osu.Game/Online/API/ILocalUserState.cs b/osu.Game/Online/API/ILocalUserState.cs new file mode 100644 index 0000000000..3ccec1c9ae --- /dev/null +++ b/osu.Game/Online/API/ILocalUserState.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API +{ + public interface ILocalUserState + { + IBindable User { get; } + IBindableList Friends { get; } + IBindableList Blocks { get; } + + void UpdateFriends(); + void UpdateBlocks(); + } +} diff --git a/osu.Game/Online/API/LocalUserState.cs b/osu.Game/Online/API/LocalUserState.cs new file mode 100644 index 0000000000..81028673cf --- /dev/null +++ b/osu.Game/Online/API/LocalUserState.cs @@ -0,0 +1,128 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; + +namespace osu.Game.Online.API +{ + public partial class LocalUserState : Component, ILocalUserState + { + public IBindable User => localUser; + public IBindableList Friends => friends; + public IBindableList Blocks => blocks; + + private readonly IAPIProvider api; + + private readonly Bindable localUser = new Bindable(createGuestUser()); + private readonly BindableList friends = new BindableList(); + private readonly BindableList blocks = new BindableList(); + + private readonly Bindable configStatus = new Bindable(); + private readonly Bindable configSupporter = new Bindable(); + + public LocalUserState(IAPIProvider api, OsuConfigManager config) + { + this.api = api; + + config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + config.BindWith(OsuSetting.WasSupporter, configSupporter); + } + + #region Logging in / out + + private static APIUser createGuestUser() => new GuestUser(); + + /// + /// Show a placeholder user if saved credentials are available. + /// This is useful for storing local scores and showing a placeholder username after starting the game, + /// until a valid connection has been established. + /// + public void SetPlaceholderLocalUser(string username) + { + if (!localUser.IsDefault) + return; + + localUser.Value = new APIUser + { + Username = username, + IsSupporter = configSupporter.Value, + }; + } + + public void SetLocalUser(APIMe me) + { + localUser.Value = me; + configSupporter.Value = me.IsSupporter; + + UpdateFriends(); + UpdateBlocks(); + } + + public void ClearLocalUser() + { + // Reset the status to be broadcast on the next login, in case multiple players share the same system. + configStatus.Value = UserStatus.Online; + + // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present + Schedule(() => + { + localUser.Value = createGuestUser(); + configSupporter.Value = false; + friends.Clear(); + }); + } + + #endregion + + public void UpdateFriends() + { + if (!api.IsLoggedIn) + return; + + var friendsReq = new GetFriendsRequest(); + friendsReq.Success += res => + { + var existingFriends = friends.Select(f => f.TargetID).ToHashSet(); + var updatedFriends = res.Select(f => f.TargetID).ToHashSet(); + + // Add new friends into local list. + friends.AddRange(res.Where(r => !existingFriends.Contains(r.TargetID))); + + // Remove non-friends from local list. + friends.RemoveAll(f => !updatedFriends.Contains(f.TargetID)); + }; + + api.Queue(friendsReq); + } + + public void UpdateBlocks() + { + if (!api.IsLoggedIn) + return; + + var blocksReq = new GetBlocksRequest(); + blocksReq.Success += res => + { + var existingBlocks = blocks.Select(f => f.TargetID).ToHashSet(); + var updatedBlocks = res.Select(f => f.TargetID).ToHashSet(); + + // Add new blocked users to local list. + blocks.AddRange(res.Where(r => !existingBlocks.Contains(r.TargetID))); + + // Remove non-blocked users from local list. + blocks.RemoveAll(b => !updatedBlocks.Contains(b.TargetID)); + + // Remove friends who got blocked since last check. + friends.RemoveAll(f => updatedBlocks.Contains(f.TargetID)); + }; + + api.Queue(blocksReq); + } + } +} diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 0ab8fb205a..5ba5b48e59 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange); - friends.BindTo(api.Friends); + friends.BindTo(api.LocalUserState.Friends); friends.BindCollectionChanged(onFriendsChanged, true); friendPresences.BindTo(metadataClient.FriendPresences); diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0f29163e39..bc617cae80 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -103,7 +103,7 @@ namespace osu.Game.Online.Leaderboards private void load(IAPIProvider api, OsuColour colour) { var user = Score.User; - bool isUserFriend = api.Friends.Any(friend => friend.TargetID == user.OnlineID); + bool isUserFriend = api.LocalUserState.Friends.Any(friend => friend.TargetID == user.OnlineID); statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList(); diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index bd39cf0253..59a4985d08 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -212,7 +212,7 @@ namespace osu.Game.Overlays.Chat items.Add(new OsuMenuItemSpacer()); items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); - items.Add(api.Blocks.Any(b => b.TargetID == user.OnlineID) + items.Add(api.LocalUserState.Blocks.Any(b => b.TargetID == user.OnlineID) ? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Unblock(user))) : new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Block(user)))); diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 941d293d9d..56cf9fc669 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -162,7 +162,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - apiFriends.BindTo(api.Friends); + apiFriends.BindTo(api.LocalUserState.Friends); apiFriends.BindCollectionChanged((_, _) => reloadList()); userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList(), true); diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs index 763571f605..b58b486494 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - apiFriends.BindTo(api.Friends); + apiFriends.BindTo(api.LocalUserState.Friends); apiFriends.BindCollectionChanged((_, _) => updateCounts()); friendPresences.BindTo(metadataClient.FriendPresences); diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index daf23c8ef3..4ebedbf946 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -101,7 +101,7 @@ namespace osu.Game.Overlays.Profile.Header.Components status.Value = FriendStatus.None; } - api.UpdateLocalFriends(); + api.LocalUserState.UpdateFriends(); HideLoadingLayer(); }; @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { base.LoadComplete(); - apiFriends.BindTo(api.Friends); + apiFriends.BindTo(api.LocalUserState.Friends); apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus)); User.BindValueChanged(u => diff --git a/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs b/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs index b8e7e96665..1a2593cff7 100644 --- a/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/UserActionsButton.cs @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Background.Colour = colourProvider.Background6; - bool userBlocked = api.Blocks.Any(b => b.TargetID == user.Id); + bool userBlocked = api.LocalUserState.Blocks.Any(b => b.TargetID == user.Id); AllowableAnchors = [Anchor.BottomCentre, Anchor.TopCentre]; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 8fcb09723e..65805a970d 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -164,7 +164,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (s.UserID == api.LocalUser.Value.Id) highlightType = BeatmapLeaderboardScore.HighlightType.Own; - else if (api.Friends.Any(r => r.TargetID == s.UserID)) + else if (api.LocalUserState.Friends.Any(r => r.TargetID == s.UserID)) highlightType = BeatmapLeaderboardScore.HighlightType.Friend; return new BeatmapLeaderboardScore(s, sheared: false) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index 8568c096d2..1480e866a6 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -458,7 +458,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null; bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true; - bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID); + bool isUserBlocked() => api.LocalUserState.Blocks.Any(b => b.TargetID == User.OnlineID); } } } diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index e4f8d5ebc3..339488e5d0 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -303,7 +303,7 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.TargetID); + isFriend = User != null && api.LocalUserState.Friends.Any(u => User.OnlineID == u.TargetID); scoreDisplayMode = config.GetBindable(OsuSetting.ScoreDisplayMode); scoreDisplayMode.BindValueChanged(_ => updateScore()); diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 0c21d4f6ed..8aa3a0516f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -298,7 +298,7 @@ namespace osu.Game.Screens.SelectV2 if (s.OnlineID == userScore?.OnlineID) highlightType = BeatmapLeaderboardScore.HighlightType.Own; - else if (api.Friends.Any(r => r.TargetID == s.UserID) && Scope.Value != BeatmapLeaderboardScope.Friend) + else if (api.LocalUserState.Friends.Any(r => r.TargetID == s.UserID) && Scope.Value != BeatmapLeaderboardScope.Friend) highlightType = BeatmapLeaderboardScore.HighlightType.Friend; return new BeatmapLeaderboardScore(s) diff --git a/osu.Game/Users/ConfirmBlockActionDialog.cs b/osu.Game/Users/ConfirmBlockActionDialog.cs index 4dccc77ebc..9c52f0e844 100644 --- a/osu.Game/Users/ConfirmBlockActionDialog.cs +++ b/osu.Game/Users/ConfirmBlockActionDialog.cs @@ -41,7 +41,7 @@ namespace osu.Game.Users req.Success += () => { - api.UpdateLocalBlocks(); + api.LocalUserState.UpdateBlocks(); }; req.Failure += e => diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 808958311c..822eac7258 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -186,7 +186,7 @@ namespace osu.Game.Users bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null; bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true; - bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID); + bool isUserBlocked() => api.LocalUserState.Blocks.Any(b => b.TargetID == User.OnlineID); } }