diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index a8369dd6d9..55e6b54af7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -8,10 +8,12 @@ using System.Linq; using System.Collections.Generic; using System.Net; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -30,6 +32,7 @@ using osu.Game.Overlays.Chat.Listing; using osu.Game.Overlays.Chat.ChannelList; using osuTK; using osuTK.Input; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Tests.Visual.Online { @@ -53,6 +56,9 @@ namespace osu.Game.Tests.Visual.Online private int currentMessageId; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim(); + [SetUp] public void SetUp() => Schedule(() => { @@ -576,6 +582,75 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestChatReport() + { + ChatReportRequest request = null; + + AddStep("Show overlay with channel", () => + { + chatOverlay.Show(); + channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1); + }); + + AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); + waitForChannel1Visible(); + + AddStep("Setup request handling", () => + { + requestLock.Reset(); + + dummyAPI.HandleRequest = r => + { + if (!(r is ChatReportRequest req)) + return false; + + Task.Run(() => + { + request = req; + requestLock.Wait(10000); + req.TriggerSuccess(); + }); + + return true; + }; + }); + + AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); + + AddStep("Set report reason to other", () => + { + var reason = this.ChildrenOfType>().Single(); + reason.Current.Value = ChatReportReason.Other; + }); + + AddStep("Try to report", () => + { + var btn = this.ChildrenOfType().Single().ChildrenOfType().Single(); + InputManager.MoveMouseTo(btn); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("Nothing happened", () => this.ChildrenOfType().Any()); + AddStep("Set report data", () => + { + var field = this.ChildrenOfType().Single().ChildrenOfType().Single(); + field.Current.Value = "test other"; + }); + + AddStep("Try to report", () => + { + var btn = this.ChildrenOfType().Single().ChildrenOfType().Single(); + InputManager.MoveMouseTo(btn); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("Overlay closed", () => !this.ChildrenOfType().Any()); + AddStep("Complete request", () => requestLock.Set()); + AddUntilStep("Request sent", () => request != null); + AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage))); + } + private void joinTestChannel(int i) { AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i])); diff --git a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs new file mode 100644 index 0000000000..7b3c32d60d --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + /// + /// A generic popover for sending an online report about something. + /// + /// An enumeration type with all valid reasons for the report. + public abstract partial class ReportPopover : OsuPopover + where TReportReason : struct, Enum + { + /// + /// The action to run when the report is finalised. + /// The arguments to this action are: the reason for the report, and an optional additional comment. + /// + public Action? Action; + + private OsuEnumDropdown reasonDropdown = null!; + private OsuTextBox commentsTextBox = null!; + private RoundedButton submitButton = null!; + + private readonly LocalisableString header; + + /// + /// Creates a new . + /// + /// The text to display in the header of the popover. + protected ReportPopover(LocalisableString headerString) + { + header = headerString; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Child = new ReverseChildIDFillFlowContainer + { + Direction = FillDirection.Vertical, + Width = 500, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(7), + Children = new Drawable[] + { + new SpriteIcon + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Icon = FontAwesome.Solid.ExclamationTriangle, + Size = new Vector2(36), + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = header, + Font = OsuFont.Torus.With(size: 25), + Margin = new MarginPadding { Bottom = 10 } + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = UsersStrings.ReportReason, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Child = reasonDropdown = new OsuEnumDropdown + { + RelativeSizeAxes = Axes.X + } + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = UsersStrings.ReportComments, + }, + commentsTextBox = new OsuTextBox + { + RelativeSizeAxes = Axes.X, + PlaceholderText = UsersStrings.ReportPlaceholder, + }, + submitButton = new RoundedButton + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Width = 200, + BackgroundColour = colours.Red3, + Text = UsersStrings.ReportActionsSend, + Action = () => + { + Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text); + this.HidePopover(); + }, + Margin = new MarginPadding { Bottom = 5, Top = 10 }, + } + } + }; + + commentsTextBox.Current.BindValueChanged(_ => updateStatus()); + + reasonDropdown.Current.BindValueChanged(_ => updateStatus()); + + updateStatus(); + } + + private void updateStatus() + { + submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || !IsCommentRequired(reasonDropdown.Current.Value); + } + + /// + /// Determines whether an additional comment is required for submitting the report with the supplied . + /// + protected virtual bool IsCommentRequired(TReportReason reason) => true; + } +} diff --git a/osu.Game/Online/API/Requests/ChatReportRequest.cs b/osu.Game/Online/API/Requests/ChatReportRequest.cs new file mode 100644 index 0000000000..85e5559e01 --- /dev/null +++ b/osu.Game/Online/API/Requests/ChatReportRequest.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Overlays.Chat; + +namespace osu.Game.Online.API.Requests +{ + public class ChatReportRequest : APIRequest + { + public readonly long? MessageId; + public readonly ChatReportReason Reason; + public readonly string Comment; + + public ChatReportRequest(long? id, ChatReportReason reason, string comment) + { + MessageId = id; + Reason = reason; + Comment = comment; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + + req.AddParameter(@"reportable_type", @"message"); + req.AddParameter(@"reportable_id", $"{MessageId}"); + req.AddParameter(@"reason", Reason.ToString()); + req.AddParameter(@"comments", Comment); + + return req; + } + + protected override string Target => @"reports"; + } +} diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 70c3bf181c..2f4c175ac4 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -1,25 +1,28 @@ // 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 System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Chat { - public partial class ChatLine : CompositeDrawable + public partial class ChatLine : CompositeDrawable, IHasPopover { private Message message = null!; @@ -55,7 +58,7 @@ namespace osu.Game.Overlays.Chat private readonly OsuSpriteText drawableTimestamp; - private readonly DrawableUsername drawableUsername; + private readonly DrawableChatUsername drawableUsername; private readonly LinkFlowContainer drawableContentFlow; @@ -92,7 +95,7 @@ namespace osu.Game.Overlays.Chat Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, - drawableUsername = new DrawableUsername(message.Sender) + drawableUsername = new DrawableChatUsername(message.Sender) { Width = UsernameWidth, FontSize = FontSize, @@ -100,6 +103,7 @@ namespace osu.Game.Overlays.Chat Origin = Anchor.TopRight, Anchor = Anchor.TopRight, Margin = new MarginPadding { Horizontal = Spacing }, + ReportRequested = this.ShowPopover, }, drawableContentFlow = new LinkFlowContainer(styleMessageContent) { @@ -128,6 +132,8 @@ namespace osu.Game.Overlays.Chat FinishTransforms(true); } + public Popover GetPopover() => new ReportChatPopover(message); + /// /// Performs a highlight animation on this . /// diff --git a/osu.Game/Overlays/Chat/ChatReportReason.cs b/osu.Game/Overlays/Chat/ChatReportReason.cs new file mode 100644 index 0000000000..5fda11b61e --- /dev/null +++ b/osu.Game/Overlays/Chat/ChatReportReason.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Overlays.Chat +{ + /// + /// References: + /// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L50 + /// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L39 + /// + public enum ChatReportReason + { + [Description("Insulting People")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsInsults))] + Insults, + + [Description("Spam")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsSpam))] + Spam, + + [Description("Unwanted Content")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsUnwantedContent))] + UnwantedContent, + + [Description("Nonsense")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsNonsense))] + Nonsense, + + [Description("Other")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsOther))] + Other + } +} diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs similarity index 95% rename from osu.Game/Overlays/Chat/DrawableUsername.cs rename to osu.Game/Overlays/Chat/DrawableChatUsername.cs index 031a0b6ae2..4b4afc204c 100644 --- a/osu.Game/Overlays/Chat/DrawableUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -29,8 +29,10 @@ using ChatStrings = osu.Game.Localisation.ChatStrings; namespace osu.Game.Overlays.Chat { - public partial class DrawableUsername : OsuClickableContainer, IHasContextMenu + public partial class DrawableChatUsername : OsuClickableContainer, IHasContextMenu { + public Action? ReportRequested; + public Color4 AccentColour { get; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => @@ -75,7 +77,7 @@ namespace osu.Game.Overlays.Chat private readonly Drawable colouredDrawable; - public DrawableUsername(APIUser user) + public DrawableChatUsername(APIUser user) { this.user = user; @@ -169,6 +171,9 @@ namespace osu.Game.Overlays.Chat })); } + if (!user.Equals(api.LocalUser.Value)) + items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, ReportRequested)); + return items.ToArray(); } } diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs new file mode 100644 index 0000000000..265a17c799 --- /dev/null +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -0,0 +1,41 @@ +// 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.Allocation; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Overlays.Chat +{ + public partial class ReportChatPopover : ReportPopover + { + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private ChannelManager channelManager { get; set; } = null!; + + private readonly Message message; + + public ReportChatPopover(Message message) + : base(ReportStrings.UserTitle(message.Sender?.Username ?? @"Someone")) + { + this.message = message; + Action = report; + } + + protected override bool IsCommentRequired(ChatReportReason reason) => reason == ChatReportReason.Other; + + private void report(ChatReportReason reason, string comments) + { + var request = new ChatReportRequest(message.Id, reason, comments); + + request.Success += () => channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(UsersStrings.ReportThanks.ToString())); + + api.Queue(request); + } + } +} diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index b44c7c48f5..96dbfe31f3 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -134,9 +135,13 @@ namespace osu.Game.Overlays }, Children = new Drawable[] { - currentChannelContainer = new Container + new PopoverContainer { RelativeSizeAxes = Axes.Both, + Child = currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + } }, loading = new LoadingLayer(true), channelListing = new ChannelListing diff --git a/osu.Game/Overlays/Comments/CommentReportButton.cs b/osu.Game/Overlays/Comments/CommentReportButton.cs index ba5319094b..e4d4d671da 100644 --- a/osu.Game/Overlays/Comments/CommentReportButton.cs +++ b/osu.Game/Overlays/Comments/CommentReportButton.cs @@ -57,6 +57,11 @@ namespace osu.Game.Overlays.Comments link.AddLink(ReportStrings.CommentButton.ToLower(), this.ShowPopover); } + public Popover GetPopover() => new ReportCommentPopover(comment) + { + Action = report + }; + private void report(CommentReportReason reason, string comments) { var request = new CommentReportRequest(comment.Id, reason, comments); @@ -83,10 +88,5 @@ namespace osu.Game.Overlays.Comments api.Queue(request); } - - public Popover GetPopover() => new ReportCommentPopover(comment) - { - Action = report - }; } } diff --git a/osu.Game/Overlays/Comments/ReportCommentPopover.cs b/osu.Game/Overlays/Comments/ReportCommentPopover.cs index f3b2a2f97c..e688dad755 100644 --- a/osu.Game/Overlays/Comments/ReportCommentPopover.cs +++ b/osu.Game/Overlays/Comments/ReportCommentPopover.cs @@ -1,111 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; -using osuTK; namespace osu.Game.Overlays.Comments { - public partial class ReportCommentPopover : OsuPopover + public partial class ReportCommentPopover : ReportPopover { - public Action? Action; - - private readonly Comment? comment; - - private OsuEnumDropdown reasonDropdown = null!; - private OsuTextBox commentsTextBox = null!; - private RoundedButton submitButton = null!; - public ReportCommentPopover(Comment? comment) + : base(ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone")) { - this.comment = comment; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Child = new ReverseChildIDFillFlowContainer - { - Direction = FillDirection.Vertical, - Width = 500, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(7), - Children = new Drawable[] - { - new SpriteIcon - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Icon = FontAwesome.Solid.ExclamationTriangle, - Size = new Vector2(36), - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"), - Font = OsuFont.Torus.With(size: 25), - Margin = new MarginPadding { Bottom = 10 } - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = UsersStrings.ReportReason, - }, - new Container - { - RelativeSizeAxes = Axes.X, - Height = 40, - Child = reasonDropdown = new OsuEnumDropdown - { - RelativeSizeAxes = Axes.X - } - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = UsersStrings.ReportComments, - }, - commentsTextBox = new OsuTextBox - { - RelativeSizeAxes = Axes.X, - PlaceholderText = UsersStrings.ReportPlaceholder, - }, - submitButton = new RoundedButton - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 200, - BackgroundColour = colours.Red3, - Text = UsersStrings.ReportActionsSend, - Action = () => - { - Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text); - this.HidePopover(); - }, - Margin = new MarginPadding { Bottom = 5, Top = 10 }, - } - } - }; - - commentsTextBox.Current.BindValueChanged(e => - { - submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(e.NewValue); - }, true); } } }