Merge pull request #22283 from Feodor0090/comment-reply

Add ability to leave replies to comments
This commit is contained in:
Dean Herbert 2023-01-19 16:41:05 +09:00 committed by GitHub
commit c6dfbab702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 206 additions and 13 deletions

View File

@ -11,6 +11,8 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -21,6 +23,7 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Comments;
using osu.Game.Overlays.Comments.Buttons;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
@ -278,6 +281,93 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Request is correct", () => request != null && request.CommentID == 2 && request.Comment == report_text && request.Reason == CommentReportReason.Other);
}
[Test]
public void TestReply()
{
addTestComments();
DrawableComment? targetComment = null;
AddUntilStep("Comment exists", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
targetComment = comments.SingleOrDefault(x => x.Comment.Id == 2);
return targetComment != null;
});
AddStep("Setup request handling", () =>
{
requestLock.Reset();
dummyAPI.HandleRequest = r =>
{
if (!(r is CommentPostRequest req))
return false;
if (req.ParentCommentId != 2)
throw new ArgumentException("Wrong parent ID in request!");
if (req.CommentableId != 123 || req.Commentable != CommentableType.Beatmapset)
throw new ArgumentException("Wrong commentable data in request!");
Task.Run(() =>
{
requestLock.Wait(10000);
req.TriggerSuccess(new CommentBundle
{
Comments = new List<Comment>
{
new Comment
{
Id = 98,
Message = req.Message,
LegacyName = "FirstUser",
CreatedAt = DateTimeOffset.Now,
VotesCount = 98,
ParentId = req.ParentCommentId,
}
}
});
});
return true;
};
});
AddStep("Click reply button", () =>
{
var btn = targetComment.ChildrenOfType<LinkFlowContainer>().Skip(1).First();
var texts = btn.ChildrenOfType<SpriteText>();
InputManager.MoveMouseTo(texts.Skip(1).First());
InputManager.Click(MouseButton.Left);
});
AddAssert("There is 0 replies", () =>
{
var replLabel = targetComment.ChildrenOfType<ShowRepliesButton>().First().ChildrenOfType<SpriteText>().First();
return replLabel.Text.ToString().Contains('0') && targetComment!.Comment.RepliesCount == 0;
});
AddStep("Focus field", () =>
{
InputManager.MoveMouseTo(targetComment.ChildrenOfType<TextBox>().First());
InputManager.Click(MouseButton.Left);
});
AddStep("Enter text", () =>
{
targetComment.ChildrenOfType<TextBox>().First().Current.Value = "random reply";
});
AddStep("Submit", () =>
{
InputManager.Key(Key.Enter);
});
AddStep("Complete request", () => requestLock.Set());
AddUntilStep("There is 1 reply", () =>
{
var replLabel = targetComment.ChildrenOfType<ShowRepliesButton>().First().ChildrenOfType<SpriteText>().First();
return replLabel.Text.ToString().Contains('1') && targetComment!.Comment.RepliesCount == 1;
});
AddUntilStep("Submitted comment shown", () =>
{
var r = targetComment.ChildrenOfType<DrawableComment>().Skip(1).FirstOrDefault();
return r != null && r.Comment.Message == "random reply";
});
}
private void addTestComments()
{
AddStep("set up response", () =>

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Input.Events;
@ -15,7 +13,12 @@ namespace osu.Game.Overlays.Comments.Buttons
public ShowRepliesButton(int count)
{
Text = "reply".ToQuantity(count);
Count = count;
}
public int Count
{
set => Text = "reply".ToQuantity(value);
}
protected override void LoadComplete()

View File

@ -35,6 +35,8 @@ namespace osu.Game.Overlays.Comments
private RoundedButton commitButton = null!;
private LoadingSpinner loadingSpinner = null!;
protected TextBox TextBox { get; private set; } = null!;
protected bool ShowLoadingSpinner
{
set
@ -51,8 +53,6 @@ namespace osu.Game.Overlays.Comments
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
EditorTextBox textBox;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Comments
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
textBox = new EditorTextBox
TextBox = new EditorTextBox
{
Height = 40,
RelativeSizeAxes = Axes.X,
@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Comments
}
});
textBox.OnCommit += (_, _) => commitButton.TriggerClick();
TextBox.OnCommit += (_, _) => commitButton.TriggerClick();
}
protected override void LoadComplete()

View File

@ -301,7 +301,7 @@ namespace osu.Game.Overlays.Comments
void addNewComment(Comment comment)
{
var drawableComment = getDrawableComment(comment);
var drawableComment = GetDrawableComment(comment);
if (comment.ParentId == null)
{
@ -333,7 +333,7 @@ namespace osu.Game.Overlays.Comments
if (CommentDictionary.ContainsKey(comment.Id))
continue;
topLevelComments.Add(getDrawableComment(comment));
topLevelComments.Add(GetDrawableComment(comment));
}
if (topLevelComments.Any())
@ -351,7 +351,7 @@ namespace osu.Game.Overlays.Comments
}
}
private DrawableComment getDrawableComment(Comment comment)
public DrawableComment GetDrawableComment(Comment comment)
{
if (CommentDictionary.TryGetValue(comment.Id, out var existing))
return existing;

View File

@ -22,6 +22,7 @@ using System.Collections.Specialized;
using System.Diagnostics;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@ -74,6 +75,7 @@ namespace osu.Game.Overlays.Comments
private OsuSpriteText deletedLabel = null!;
private GridContainer content = null!;
private VotePill votePill = null!;
private Container<CommentEditor> replyEditorContainer = null!;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
@ -232,6 +234,12 @@ namespace osu.Game.Overlays.Comments
}
}
},
replyEditorContainer = new Container<CommentEditor>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Top = 10 },
},
new Container
{
AutoSizeAxes = Axes.Both,
@ -254,6 +262,7 @@ namespace osu.Game.Overlays.Comments
},
childCommentsVisibilityContainer = new FillFlowContainer
{
Name = @"Children comments",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
@ -344,6 +353,8 @@ namespace osu.Game.Overlays.Comments
actionsContainer.AddLink(CommonStrings.ButtonsPermalink, copyUrl);
actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
actionsContainer.AddLink(CommonStrings.ButtonsReply.ToLower(), toggleReply);
actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
actionsContainer.AddLink(CommonStrings.ButtonsDelete.ToLower(), deleteComment);
@ -419,8 +430,9 @@ namespace osu.Game.Overlays.Comments
if (!ShowDeleted.Value)
Hide();
});
request.Failure += _ => Schedule(() =>
request.Failure += e => Schedule(() =>
{
Logger.Error(e, "Failed to delete comment");
actionsLoading.Hide();
actionsContainer.Show();
});
@ -433,6 +445,26 @@ namespace osu.Game.Overlays.Comments
onScreenDisplay?.Display(new CopyUrlToast());
}
private void toggleReply()
{
if (replyEditorContainer.Count == 0)
{
replyEditorContainer.Add(new ReplyCommentEditor(Comment)
{
OnPost = comments =>
{
Comment.RepliesCount += comments.Length;
showRepliesButton.Count = Comment.RepliesCount;
Replies.AddRange(comments);
}
});
}
else
{
replyEditorContainer.Clear(true);
}
}
protected override void LoadComplete()
{
ShowDeleted.BindValueChanged(show =>
@ -445,8 +477,6 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete();
}
public bool ContainsReply(long replyId) => loadedReplies.ContainsKey(replyId);
private void onRepliesAdded(IEnumerable<DrawableComment> replies)
{
var page = createRepliesPage(replies);

View File

@ -0,0 +1,70 @@
// 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;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
{
public partial class ReplyCommentEditor : CancellableCommentEditor
{
[Resolved]
private CommentsContainer commentsContainer { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Comment parentComment;
public Action<DrawableComment[]>? OnPost;
protected override LocalisableString FooterText => default;
protected override LocalisableString CommitButtonText => CommonStrings.ButtonsReply;
protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderReply;
public ReplyCommentEditor(Comment parent)
{
parentComment = parent;
OnCancel = () => this.FadeOut(200).Expire();
}
protected override void LoadComplete()
{
base.LoadComplete();
GetContainingInputManager().ChangeFocus(TextBox);
}
protected override void OnCommit(string text)
{
ShowLoadingSpinner = true;
CommentPostRequest req = new CommentPostRequest(commentsContainer.Type.Value, commentsContainer.Id.Value, text, parentComment.Id);
req.Failure += e => Schedule(() =>
{
ShowLoadingSpinner = false;
Logger.Error(e, "Posting reply comment failed.");
});
req.Success += cb => Schedule(processPostedComments, cb);
api.Queue(req);
}
private void processPostedComments(CommentBundle cb)
{
foreach (var comment in cb.Comments)
comment.ParentComment = parentComment;
var drawables = cb.Comments.Select(commentsContainer.GetDrawableComment).ToArray();
OnPost?.Invoke(drawables);
OnCancel!.Invoke();
}
}
}