diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index cd22bb2513..31e5a9b86c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -42,19 +42,21 @@ namespace osu.Game.Tests.Visual.Online () => commentsContainer.ChildrenOfType().Single().IsLoading); } - [Test] - public void TestSingleCommentsPage() + [TestCase(false)] + [TestCase(true)] + public void TestSingleCommentsPage(bool withPinned) { - setUpCommentsResponse(exampleComments); + setUpCommentsResponse(getExampleComments(withPinned)); AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); AddUntilStep("show more button hidden", () => commentsContainer.ChildrenOfType().Single().Alpha == 0); } - [Test] - public void TestMultipleCommentPages() + [TestCase(false)] + [TestCase(true)] + public void TestMultipleCommentPages(bool withPinned) { - var comments = exampleComments; + var comments = getExampleComments(withPinned); comments.HasMore = true; comments.TopLevelCount = 10; @@ -64,11 +66,12 @@ namespace osu.Game.Tests.Visual.Online () => commentsContainer.ChildrenOfType().Single().Alpha == 1); } - [Test] - public void TestMultipleLoads() + [TestCase(false)] + [TestCase(true)] + public void TestMultipleLoads(bool withPinned) { - var comments = exampleComments; - int topLevelCommentCount = exampleComments.Comments.Count; + var comments = getExampleComments(withPinned); + int topLevelCommentCount = comments.Comments.Count; AddStep("hide container", () => commentsContainer.Hide()); setUpCommentsResponse(comments); @@ -79,6 +82,48 @@ namespace osu.Game.Tests.Visual.Online () => commentsContainer.ChildrenOfType().Count() == topLevelCommentCount); } + [Test] + public void TestNoComment() + { + var comments = getExampleComments(); + comments.Comments.Clear(); + + setUpCommentsResponse(comments); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddAssert("no comment shown", () => !commentsContainer.ChildrenOfType().Any()); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSingleComment(bool withPinned) + { + var comment = new Comment + { + Id = 1, + Message = "This is a single comment", + LegacyName = "SingleUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 0, + Pinned = withPinned, + }; + + var bundle = new CommentBundle + { + Comments = new List { comment }, + IncludedComments = new List(), + PinnedComments = new List(), + }; + + if (withPinned) + bundle.PinnedComments.Add(comment); + + setUpCommentsResponse(bundle); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddUntilStep("wait comment load", () => commentsContainer.ChildrenOfType().Any()); + AddAssert("only one comment shown", () => + commentsContainer.ChildrenOfType().Count(d => d.Comment.Pinned == withPinned) == 1); + } + private void setUpCommentsResponse(CommentBundle commentBundle) => AddStep("set up response", () => { @@ -92,38 +137,71 @@ namespace osu.Game.Tests.Visual.Online }; }); - private CommentBundle exampleComments => new CommentBundle + private CommentBundle getExampleComments(bool withPinned = false) { - Comments = new List + var bundle = new CommentBundle { - new Comment + Comments = new List { - Id = 1, - Message = "This is a comment", - LegacyName = "FirstUser", - CreatedAt = DateTimeOffset.Now, - VotesCount = 19, - RepliesCount = 1 + new Comment + { + Id = 1, + Message = "This is a comment", + LegacyName = "FirstUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 19, + RepliesCount = 1 + }, + new Comment + { + Id = 5, + ParentId = 1, + Message = "This is a child comment", + LegacyName = "SecondUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 4, + }, + new Comment + { + Id = 10, + Message = "This is another comment", + LegacyName = "ThirdUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 0 + }, }, - new Comment + IncludedComments = new List(), + PinnedComments = new List(), + }; + + if (withPinned) + { + var pinnedComment = new Comment { - Id = 5, - ParentId = 1, - Message = "This is a child comment", - LegacyName = "SecondUser", + Id = 15, + Message = "This is pinned comment", + LegacyName = "PinnedUser", CreatedAt = DateTimeOffset.Now, - VotesCount = 4, - }, - new Comment + VotesCount = 999, + Pinned = true, + RepliesCount = 1, + }; + + bundle.Comments.Add(pinnedComment); + bundle.PinnedComments.Add(pinnedComment); + + bundle.Comments.Add(new Comment { - Id = 10, - Message = "This is another comment", - LegacyName = "ThirdUser", + Id = 20, + Message = "Reply to pinned comment", + LegacyName = "AbandonedUser", CreatedAt = DateTimeOffset.Now, - VotesCount = 0 - }, - }, - IncludedComments = new List(), - }; + VotesCount = 0, + ParentId = 15, + }); + } + + return bundle; + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs index 7b741accbb..b26850feb2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs @@ -43,6 +43,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep(description, () => { + comment.Pinned = description == "Pinned"; comment.Message = text; container.Add(new DrawableComment(comment)); }); @@ -59,6 +60,7 @@ namespace osu.Game.Tests.Visual.Online private static object[] comments = { new[] { "Plain", "This is plain comment" }, + new[] { "Pinned", "This is pinned comment" }, new[] { "Link", "Please visit https://osu.ppy.sh" }, new[] diff --git a/osu.Game.Tests/Visual/Online/TestSceneOfflineCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneOfflineCommentsContainer.cs index 628ae0971b..4f7947b69c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOfflineCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOfflineCommentsContainer.cs @@ -149,6 +149,7 @@ namespace osu.Game.Tests.Visual.Online } }, IncludedComments = new List(), + PinnedComments = new List(), UserVotes = new List { 5 @@ -178,6 +179,7 @@ namespace osu.Game.Tests.Visual.Online }, }, IncludedComments = new List(), + PinnedComments = new List(), }; private class TestCommentsContainer : CommentsContainer diff --git a/osu.Game/Online/API/Requests/Responses/Comment.cs b/osu.Game/Online/API/Requests/Responses/Comment.cs index 05a24cec0e..32d489432d 100644 --- a/osu.Game/Online/API/Requests/Responses/Comment.cs +++ b/osu.Game/Online/API/Requests/Responses/Comment.cs @@ -58,6 +58,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"edited_by_id")] public long? EditedById { get; set; } + [JsonProperty(@"pinned")] + public bool Pinned { get; set; } + public User EditedUser { get; set; } public bool IsTopLevel => !ParentId.HasValue; diff --git a/osu.Game/Online/API/Requests/Responses/CommentBundle.cs b/osu.Game/Online/API/Requests/Responses/CommentBundle.cs index d76ede67cd..4070df493d 100644 --- a/osu.Game/Online/API/Requests/Responses/CommentBundle.cs +++ b/osu.Game/Online/API/Requests/Responses/CommentBundle.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using osu.Game.Users; using System.Collections.Generic; +using System.Linq; namespace osu.Game.Online.API.Requests.Responses { @@ -24,6 +25,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"included_comments")] public List IncludedComments { get; set; } + [JsonProperty(@"pinned_comments")] + public List PinnedComments { get; set; } + private List userVotes; [JsonProperty(@"user_votes")] @@ -49,26 +53,17 @@ namespace osu.Game.Online.API.Requests.Responses { users = value; - value.ForEach(u => + foreach (var user in value) { - Comments.ForEach(c => + foreach (var comment in Comments.Concat(IncludedComments).Concat(PinnedComments)) { - if (c.UserId == u.Id) - c.User = u; + if (comment.UserId == user.Id) + comment.User = user; - if (c.EditedById == u.Id) - c.EditedUser = u; - }); - - IncludedComments.ForEach(c => - { - if (c.UserId == u.Id) - c.User = u; - - if (c.EditedById == u.Id) - c.EditedUser = u; - }); - }); + if (comment.EditedById == user.Id) + comment.EditedUser = user; + } + } } } diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index fe8d6f0178..6b3d816e84 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -38,6 +38,7 @@ namespace osu.Game.Overlays.Comments private CancellationTokenSource loadCancellation; private int currentPage; + private FillFlowContainer pinnedContent; private FillFlowContainer content; private DeletedCommentsCounter deletedCommentsCounter; private CommentsShowMoreButton moreButton; @@ -63,6 +64,25 @@ namespace osu.Game.Overlays.Comments Children = new Drawable[] { commentCounter = new TotalCommentsCounter(), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + pinnedContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, + }, + }, new CommentsHeader { Sort = { BindTarget = Sort }, @@ -173,6 +193,7 @@ namespace osu.Game.Overlays.Comments deletedCommentsCounter.Count.Value = 0; moreButton.Show(); moreButton.IsLoading = true; + pinnedContent.Clear(); content.Clear(); CommentDictionary.Clear(); } @@ -202,7 +223,7 @@ namespace osu.Game.Overlays.Comments var topLevelComments = new List(); var orphaned = new List(); - foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments)) + foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments).Concat(bundle.PinnedComments)) { // Exclude possible duplicated comments. if (CommentDictionary.ContainsKey(comment.Id)) @@ -219,13 +240,15 @@ namespace osu.Game.Overlays.Comments { LoadComponentsAsync(topLevelComments, loaded => { - content.AddRange(loaded); + pinnedContent.AddRange(loaded.Where(d => d.Comment.Pinned)); + content.AddRange(loaded.Where(d => !d.Comment.Pinned)); deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel); if (bundle.HasMore) { int loadedTopLevelComments = 0; + pinnedContent.Children.OfType().ForEach(p => loadedTopLevelComments++); content.Children.OfType().ForEach(p => loadedTopLevelComments++); moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments; @@ -300,11 +323,6 @@ namespace osu.Game.Overlays.Comments RelativeSizeAxes = Axes.X; AddRangeInternal(new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 - }, new OsuSpriteText { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 3520b15b1e..a44f3a7643 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -21,6 +21,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Specialized; using osu.Framework.Localisation; using osu.Game.Overlays.Comments.Buttons; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Comments { @@ -137,12 +138,13 @@ namespace osu.Game.Overlays.Comments AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(10, 0), - Children = new Drawable[] + Children = new[] { username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)) { AutoSizeAxes = Axes.Both }, + Comment.Pinned ? new PinnedCommentNotice() : Empty(), new ParentUsername(Comment), new OsuSpriteText { @@ -321,9 +323,7 @@ namespace osu.Game.Overlays.Comments this.FadeTo(show.NewValue ? 1 : 0); }, true); childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true); - updateButtonsState(); - base.LoadComplete(); } @@ -392,6 +392,33 @@ namespace osu.Game.Overlays.Comments }; } + private class PinnedCommentNotice : FillFlowContainer + { + public PinnedCommentNotice() + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Horizontal; + Spacing = new Vector2(2, 0); + Children = new Drawable[] + { + new SpriteIcon + { + Icon = FontAwesome.Solid.Thumbtack, + Size = new Vector2(14), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), + Text = CommentsStrings.Pinned, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + } + private class ParentUsername : FillFlowContainer, IHasTooltip { public LocalisableString TooltipText => getParentMessage();