diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneBubbleChatHistory.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneBubbleChatHistory.cs index 8d06e9d75feb..9bb018b0ead8 100644 --- a/osu.Game.Tests/Visual/RankedPlay/TestSceneBubbleChatHistory.cs +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneBubbleChatHistory.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; namespace osu.Game.Tests.Visual.RankedPlay @@ -27,7 +28,11 @@ public void Setup() => Schedule(() => public void TestPostMessages() { int messageId = 1; - AddRepeatStep("post message", () => history.PostMessage(new APIUser { Id = 2 }, $"message {messageId++}"), 20); + AddRepeatStep("post message", () => history.PostMessage(new Message + { + Sender = new APIUser { Id = 2 }, + Content = $"message {messageId++}" + }), 20); } [Test] @@ -38,7 +43,13 @@ public void TestCollapse() AddStep("post some messages", () => { for (int i = 0; i < 10; i++) - history.PostMessage(new APIUser { Id = 2 }, $"message {i}"); + { + history.PostMessage(new Message + { + Sender = new APIUser { Id = 2 }, + Content = $"message {i}", + }); + } }); AddWaitStep("wait a bit", 10); diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayChat.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayChat.cs index 839d51249997..c8c725bd1cbd 100644 --- a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayChat.cs +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayChat.cs @@ -65,6 +65,16 @@ public void TestDiscardCardStage() postLocalUserMessage("this is a message from the local user"); postOpponentMessage("this is a message from the opponent"); + AddStep("add long message", () => testChannel.AddNewMessages(new Message(messageIdSequence++) + { + Timestamp = DateTimeOffset.Now, + Sender = new APIUser + { + Id = 2, + Username = "peppy" + }, + Content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget neque non leo placerat sollicitudin eget sit amet sem. Aenean ut ipsum et nulla lobortis viverra ut eget odio.", + })); } [Test] diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayChatDisplay.cs index 2ef1a5cc5820..fd78a3799d09 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayChatDisplay.cs @@ -7,20 +7,27 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Users.Drawables; using osuTK; @@ -28,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components { - public partial class RankedPlayChatDisplay : VisibilityContainer, IKeyBindingHandler + public partial class RankedPlayChatDisplay : VisibilityContainer, IKeyBindingHandler, IFocusManager { [Resolved] private ChannelManager? channelManager { get; set; } @@ -38,48 +45,63 @@ public partial class RankedPlayChatDisplay : VisibilityContainer, IKeyBindingHan private readonly MultiplayerRoom room; + private Container content = null!; private ChatTextBox textbox = null!; private BubbleChatHistory chatHistory = null!; private Channel? channel; + private IFocusManager parentFocusManager = null!; + private const float width = 320; public RankedPlayChatDisplay(MultiplayerRoom room) { - Size = new Vector2(width, 160); + AutoSizeAxes = Axes.Both; this.room = room; } [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChild = new ChatContextMenuContainer { - textbox = new ChatTextBox + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Child = content = new Container { + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - RelativeSizeAxes = Axes.X, - Height = 30, - CornerRadius = 10, - ReleaseFocusOnCommit = true, - HoldFocus = false, - Focus = onFocusGained, - FocusLost = onFocusLost - }, - new Container - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Bottom = 35 }, - Child = chatHistory = new BubbleChatHistory + Children = new Drawable[] { - RelativeSizeAxes = Axes.X + textbox = new ChatTextBox + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Width = width, + Height = 30, + CornerRadius = 10, + ReleaseFocusOnCommit = true, + HoldFocus = false, + Focus = onFocusGained, + FocusLost = onFocusLost + }, + new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + AutoSizeAxes = Axes.Y, + Width = width * 1.5f, + Padding = new MarginPadding { Bottom = 35 }, + Child = chatHistory = new BubbleChatHistory + { + RelativeSizeAxes = Axes.X + } + } } - } + }, }; } @@ -87,6 +109,8 @@ protected override void LoadComplete() { base.LoadComplete(); + parentFocusManager = GetContainingFocusManager()!; + resetPlaceholderText(); textbox.OnCommit += onCommit; @@ -113,7 +137,7 @@ private void onCommit(TextBox sender, bool newText) private void onNewMessagesArrived(IEnumerable bundle) { foreach (var message in bundle) - chatHistory.PostMessage(message.Sender, message.Content); + chatHistory.PostMessage(message); } private void onFocusGained() @@ -163,6 +187,20 @@ public void OnReleased(KeyBindingReleaseEvent e) { } + public void TriggerFocusContention(Drawable? triggerSource) + { + if (triggerSource == null || triggerSource.IsRootedAt(content)) + parentFocusManager.TriggerFocusContention(triggerSource); + } + + public bool ChangeFocus(Drawable? potentialFocusTarget) + { + if (potentialFocusTarget == null || potentialFocusTarget.IsRootedAt(content)) + return parentFocusManager.ChangeFocus(potentialFocusTarget); + + return false; + } + protected override void PopIn() { FinishTransforms(); @@ -189,6 +227,15 @@ protected override void Dispose(bool isDisposing) channel.NewMessagesArrived -= onNewMessagesArrived; } + private partial class ChatContextMenuContainer : OsuContextMenuContainer + { + public ChatContextMenuContainer() + { + Content.Anchor = Anchor.BottomRight; + Content.Origin = Anchor.BottomRight; + } + } + private partial class ChatTextBox : StandAloneChatDisplay.ChatTextBox { protected override void LoadComplete() @@ -220,7 +267,7 @@ public partial class BubbleChatHistory : CompositeDrawable private readonly Container messageContainer; - private bool expanded; + private readonly BindableBool expanded = new BindableBool(); private Sample messageReceivedSample = null!; private double? lastSamplePlayback; @@ -247,7 +294,7 @@ private void load(AudioManager audio) /// public void Collapse() { - expanded = false; + expanded.Value = false; foreach (var child in messageContainer.Reverse().Take(max_length).Reverse()) { @@ -265,7 +312,7 @@ public void Collapse() /// public void Expand() { - expanded = true; + expanded.Value = true; foreach (var child in messageContainer.Reverse().Take(max_length)) child.Show(); @@ -274,15 +321,15 @@ public void Expand() /// /// Posts a message. /// - /// The user that posted the message. - /// The message content. - public void PostMessage(APIUser user, string content) + /// The message. + public void PostMessage(Message message) { - var newMessage = new MessageBubble(user, content) + var newMessage = new MessageBubble(message) { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - PostTime = Time.Current + PostTime = Time.Current, + Expanded = { BindTarget = expanded }, }; messageContainer.Add(newMessage); @@ -314,7 +361,7 @@ public void PostMessage(APIUser user, string content) playSample(); // If not in the expanded state, hide the new message after a short while. - if (!expanded) + if (!expanded.Value) { using (BeginDelayedSequence(time_before_disappear)) newMessage.Hide(); @@ -330,19 +377,25 @@ private void playSample() lastSamplePlayback = Time.Current; } - private partial class MessageBubble : CompositeDrawable + private partial class MessageBubble : CompositeDrawable, IHasContextMenu, IHasPopover { - private readonly APIUser user; - private readonly string message; + private readonly Message message; /// /// The time at which this message was posted. /// public required double PostTime { get; init; } - public MessageBubble(APIUser user, string message) + /// + /// Whether the message history is currently in an expanded state. + /// + public readonly IBindable Expanded = new BindableBool(); + + private const int text_offset = 20; + private const int padding = 8; + + public MessageBubble(Message message) { - this.user = user; this.message = message; AutoSizeAxes = Axes.Both; @@ -368,14 +421,14 @@ private void load() new Box { RelativeSizeAxes = Axes.Both, - Colour = api.LocalUser.Value.Id == user.Id + Colour = api.LocalUser.Value.Id == message.SenderId ? RankedPlayColourScheme.BLUE.PrimaryDarkest : RankedPlayColourScheme.RED.PrimaryDarkest, }, new Container { AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(8), + Padding = new MarginPadding(padding), Children = new Drawable[] { new CircularContainer @@ -384,7 +437,7 @@ private void load() Origin = Anchor.CentreLeft, Size = new Vector2(16), Masking = true, - Child = new UpdateableAvatar(user) + Child = new UpdateableAvatar(message.Sender) { DelayedLoad = false, RelativeSizeAxes = Axes.Both @@ -392,12 +445,12 @@ private void load() }, new OsuTextFlowContainer { - X = 20, - MaximumSize = new Vector2(width * 1.5f, 0), + X = text_offset, + MaximumSize = new Vector2(width * 1.5f - text_offset - padding * 2, 0), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Both, - Text = message, + Text = message.Content, } } } @@ -416,6 +469,31 @@ public override void Hide() { this.FadeOut(200, Easing.OutQuint); } + + public MenuItem[]? ContextMenuItems + { + get + { + if (!Expanded.Value) + return null; + + if (message.Sender.Equals(APIUser.SYSTEM_USER)) + return null; + + if (message.Sender.Equals(api.LocalUser.Value)) + return null; + + return [new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, this.ShowPopover)]; + } + } + + public Popover? GetPopover() + { + if (message.Sender.Equals(api.LocalUser.Value)) + return null; + + return new ReportChatPopover(message); + } } } }