diff --git a/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs new file mode 100644 index 0000000000..1f7a7e7165 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs @@ -0,0 +1,161 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile.Sections; +using osu.Game.Overlays.Profile.Sections.Recent; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseUserProfileRecentSection : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(RecentSection), + typeof(DrawableRecentActivity), + typeof(PaginatedRecentActivityContainer), + typeof(MedalIcon) + }; + + public TestCaseUserProfileRecentSection() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.2f) + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + ChildrenEnumerable = createDummyActivities().Select(a => new DrawableRecentActivity(a)) + }, + } + }; + } + + private IEnumerable createDummyActivities() + { + var dummyBeatmap = new RecentActivity.RecentActivityBeatmap + { + Title = @"Dummy beatmap", + Url = "/b/1337", + }; + + var dummyUser = new RecentActivity.RecentActivityUser + { + Username = "DummyReborn", + Url = "/u/666", + PreviousUsername = "Dummy", + }; + + return new[] + { + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.Achievement, + Achievement = new RecentActivity.RecentActivityAchievement + { + Name = @"Feelin' It", + Slug = @"all-secret-feelinit", + }, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapPlaycount, + Count = 1337, + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetApprove, + Approval = BeatmapApproval.Qualified, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetDelete, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetRevive, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetRevive, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetUpdate, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetUpload, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.Rank, + Rank = 1, + Mode = "osu!", + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.RankLost, + Mode = "osu!", + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UsernameChange, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportAgain, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportFirst, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportGift, + }, + }; + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 8cbeb6aab6..1cfa7bc111 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -173,6 +173,7 @@ + diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 9f1b44af44..1d231ada23 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -90,6 +90,10 @@ namespace osu.Game.Graphics.Containers case LinkAction.External: Process.Start(url); break; + case LinkAction.OpenUserProfile: + if (long.TryParse(linkArgument, out long userId)) + game?.ShowUser(userId); + break; default: throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action."); } diff --git a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs new file mode 100644 index 0000000000..d1685b01f3 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Rulesets.Scoring; +using Humanizer; +using System; +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests +{ + public class GetUserRecentActivitiesRequest : APIRequest> + { + private readonly long userId; + private readonly int offset; + + public GetUserRecentActivitiesRequest(long userId, int offset = 0) + { + this.userId = userId; + this.offset = offset; + } + + protected override string Target => $"users/{userId}/recent_activity?offset={offset}"; + } + + public class RecentActivity + { + [JsonProperty("id")] + public int ID; + + [JsonProperty("createdAt")] + public DateTimeOffset CreatedAt; + + [JsonProperty] + private string type + { + set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize()); + } + + public RecentActivityType Type; + + [JsonProperty] + private string scoreRank + { + set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value); + } + + public ScoreRank ScoreRank; + + [JsonProperty("rank")] + public int Rank; + + [JsonProperty("approval")] + public BeatmapApproval Approval; + + [JsonProperty("count")] + public int Count; + + [JsonProperty("mode")] + public string Mode; + + [JsonProperty("beatmap")] + public RecentActivityBeatmap Beatmap; + + [JsonProperty("beatmapset")] + public RecentActivityBeatmap Beatmapset; + + [JsonProperty("user")] + public RecentActivityUser User; + + [JsonProperty("achievement")] + public RecentActivityAchievement Achievement; + + public class RecentActivityBeatmap + { + [JsonProperty("title")] + public string Title; + + [JsonProperty("url")] + public string Url; + } + + public class RecentActivityUser + { + [JsonProperty("username")] + public string Username; + + [JsonProperty("url")] + public string Url; + + [JsonProperty("previousUsername")] + public string PreviousUsername; + } + + public class RecentActivityAchievement + { + [JsonProperty("slug")] + public string Slug; + + [JsonProperty("name")] + public string Name; + } + + } + + public enum RecentActivityType + { + Achievement, + BeatmapPlaycount, + BeatmapsetApprove, + BeatmapsetDelete, + BeatmapsetRevive, + BeatmapsetUpdate, + BeatmapsetUpload, + Medal, + Rank, + RankLost, + UserSupportAgain, + UserSupportFirst, + UserSupportGift, + UsernameChange, + } + + public enum BeatmapApproval + { + Ranked, + Approved, + Qualified, + } +} diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 906f42d50e..9966f78435 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -118,6 +118,8 @@ namespace osu.Game.Online.Chat case "beatmapsets": case "d": return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]); + case "u": + return new LinkDetails(LinkAction.OpenUserProfile, args[3]); } } @@ -146,6 +148,9 @@ namespace osu.Game.Online.Chat case "spectate": linkType = LinkAction.Spectate; break; + case "u": + linkType = LinkAction.OpenUserProfile; + break; default: linkType = LinkAction.External; break; @@ -205,6 +210,15 @@ namespace osu.Game.Online.Chat return inputMessage; } + public static MessageFormatterResult FormatText(string text) + { + var result = format(text); + + result.Links.Sort(); + + return result; + } + public class MessageFormatterResult { public List Links = new List(); @@ -239,6 +253,7 @@ namespace osu.Game.Online.Chat OpenEditorTimestamp, JoinMultiplayerMatch, Spectate, + OpenUserProfile, } public class Link : IComparable diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e2bc240e8c..e656c7256e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -155,6 +155,12 @@ namespace osu.Game /// The set to display. public void ShowBeatmapSet(int setId) => beatmapSetOverlay.ShowBeatmapSet(setId); + /// + /// Show a user's profile as an overlay. + /// + /// The user to display. + public void ShowUser(long userId) => userProfile.ShowUser(userId); + protected void LoadScore(Score s) { scoreLoad?.Cancel(); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs new file mode 100644 index 0000000000..2dde8a3d54 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -0,0 +1,165 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; +using osu.Game.Screens.Select.Leaderboards; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class DrawableRecentActivity : DrawableProfileRow + { + private APIAccess api; + + private readonly RecentActivity activity; + + private LinkFlowContainer content; + + public DrawableRecentActivity(RecentActivity activity) + { + this.activity = activity; + } + + [BackgroundDependencyLoader] + private void load(APIAccess api) + { + this.api = api; + + LeftFlowContainer.Padding = new MarginPadding { Left = 10, Right = 160 }; + + LeftFlowContainer.Add(content = new LinkFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }); + + RightFlowContainer.Add(new OsuSpriteText + { + Text = activity.CreatedAt.LocalDateTime.ToShortDateString(), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = "Exo2.0-RegularItalic", + TextSize = 12, + Colour = OsuColour.Gray(0xAA), + }); + + var formatted = createMessage(); + + content.AddLinks(formatted.Text, formatted.Links); + } + + protected override Drawable CreateLeftVisual() + { + switch (activity.Type) + { + case RecentActivityType.Rank: + return new DrawableRank(activity.ScoreRank) + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + + case RecentActivityType.Achievement: + return new MedalIcon(activity.Achievement.Slug) + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + + default: + return new Container + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + } + } + + private string toAbsoluteUrl(string url) => $"{api.Endpoint}{url}"; + + private MessageFormatter.MessageFormatterResult createMessage() + { + string userLinkTemplate() => $"[{toAbsoluteUrl(activity.User?.Url)} {activity.User?.Username}]"; + string beatmapLinkTemplate() => $"[{toAbsoluteUrl(activity.Beatmap?.Url)} {activity.Beatmap?.Title}]"; + string beatmapsetLinkTemplate() => $"[{toAbsoluteUrl(activity.Beatmapset?.Url)} {activity.Beatmapset?.Title}]"; + + string message; + + switch (activity.Type) + { + case RecentActivityType.Achievement: + message = $"{userLinkTemplate()} unlocked the {activity.Achievement.Name} medal!"; + break; + + case RecentActivityType.BeatmapPlaycount: + message = $"{beatmapLinkTemplate()} has been played {activity.Count} times!"; + break; + + case RecentActivityType.BeatmapsetApprove: + message = $"{beatmapsetLinkTemplate()} has been {activity.Approval.ToString().ToLowerInvariant()}!"; + break; + + case RecentActivityType.BeatmapsetDelete: + message = $"{beatmapsetLinkTemplate()} has been deleted."; + break; + + case RecentActivityType.BeatmapsetRevive: + message = $"{beatmapsetLinkTemplate()} has been revived from eternal slumber by {userLinkTemplate()}."; + break; + + case RecentActivityType.BeatmapsetUpdate: + message = $"{userLinkTemplate()} has updated the beatmap {beatmapsetLinkTemplate()}!"; + break; + + case RecentActivityType.BeatmapsetUpload: + message = $"{userLinkTemplate()} has submitted a new beatmap {beatmapsetLinkTemplate()}!"; + break; + + case RecentActivityType.Medal: + // apparently this shouldn't exist look at achievement instead (https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/profile-page/recent-activity.coffee#L111) + message = string.Empty; + break; + + case RecentActivityType.Rank: + message = $"{userLinkTemplate()} achieved rank #{activity.Rank} on {beatmapLinkTemplate()} ({activity.Mode}!)"; + break; + + case RecentActivityType.RankLost: + message = $"{userLinkTemplate()} has lost first place on {beatmapLinkTemplate()} ({activity.Mode}!)"; + break; + + case RecentActivityType.UserSupportAgain: + message = $"{userLinkTemplate()} has once again chosen to support osu! - thanks for your generosity!"; + break; + + case RecentActivityType.UserSupportFirst: + message = $"{userLinkTemplate()} has become an osu! supporter - thanks for your generosity!"; + break; + + case RecentActivityType.UserSupportGift: + message = $"{userLinkTemplate()} has received the gift of osu! supporter!"; + break; + + case RecentActivityType.UsernameChange: + message = $"{activity.User?.PreviousUsername} has changed their username to {userLinkTemplate()}!"; + break; + + default: + message = string.Empty; + break; + } + + return MessageFormatter.FormatText(message); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs new file mode 100644 index 0000000000..6ffbe7193f --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class MedalIcon : Container + { + private readonly string slug; + private readonly Sprite sprite; + + private string url => $@"https://s.ppy.sh/images/medals-client/{slug}@2x.png"; + + public MedalIcon(string slug) + { + this.slug = slug; + + Child = sprite = new Sprite + { + Height = 40, + Width = 40, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + sprite.Texture = textures.Get(url); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs new file mode 100644 index 0000000000..d479627cde --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests; +using osu.Game.Users; +using System.Linq; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class PaginatedRecentActivityContainer : PaginatedContainer + { + public PaginatedRecentActivityContainer(Bindable user, string header, string missing) + : base(user, header, missing) + { + ItemsPerPage = 5; + } + + protected override void ShowMore() + { + base.ShowMore(); + + var req = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + + req.Success += activities => + { + ShowMoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); + ShowMoreLoading.Hide(); + + if (!activities.Any() && VisiblePages == 1) + { + MissingText.Show(); + return; + } + + MissingText.Hide(); + + foreach (RecentActivity activity in activities) + { + ItemsContainer.Add(new DrawableRecentActivity(activity)); + } + }; + + Api.Queue(req); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs index 78b139efe8..84a941aa1a 100644 --- a/osu.Game/Overlays/Profile/Sections/RecentSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -1,12 +1,22 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Overlays.Profile.Sections.Recent; + namespace osu.Game.Overlays.Profile.Sections { public class RecentSection : ProfileSection { public override string Title => "Recent"; - public override string Identifier => "recent_activities"; + public override string Identifier => "recent_activity"; + + public RecentSection() + { + Children = new[] + { + new PaginatedRecentActivityContainer(User, null, @"This user hasn't done anything notable recently!"), + }; + } } } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 59f940a19d..f3fd7aeac5 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -73,6 +73,14 @@ namespace osu.Game.Overlays FadeEdgeEffectTo(0, DISAPPEAR_DURATION, Easing.Out); } + public void ShowUser(long userId) + { + if (userId == Header.User.Id) + return; + + ShowUser(new User { Id = userId }); + } + public void ShowUser(User user, bool fetchOnline = true) { userReq?.Cancel(); @@ -82,7 +90,7 @@ namespace osu.Game.Overlays sections = new ProfileSection[] { //new AboutSection(), - //new RecentSection(), + new RecentSection(), new RanksSection(), //new MedalsSection(), new HistoricalSection(), diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b8ada7c017..6f7c92ab5a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -294,12 +294,16 @@ + 20180125143340_Settings.cs + + +