From de393f735fdb3d2f4b93939af1a47f8a17217a17 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Fri, 29 Apr 2022 21:33:32 +0100 Subject: [PATCH 01/20] Implement basic layout and behaviour of new chat overlay Provides initial implementation of new chat overlay in component `ChatOverlayV2`. Contains only the basic functionality required for a functioning chat overlay according to the new design with the intent of added the rest of the functionality in subsequent PRs. Backports existing tests for the current chat overlay except for ones testing keyboard shortcuts (since they haven't been added) and tab closing behaviour (since no tabs). --- .../Visual/Online/TestSceneChatOverlayV2.cs | 365 ++++++++++++++++++ .../Chat/ChannelList/ChannelListItem.cs | 18 +- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 67 ++++ .../Chat/Listing/ChannelListingItem.cs | 18 +- osu.Game/Overlays/ChatOverlayV2.cs | 293 ++++++++++++++ 5 files changed, 743 insertions(+), 18 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs create mode 100644 osu.Game/Overlays/Chat/ChatOverlayTopBar.cs create mode 100644 osu.Game/Overlays/ChatOverlayV2.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs new file mode 100644 index 0000000000..312bda6460 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -0,0 +1,365 @@ +// 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 System.Linq; +using System.Collections.Generic; +using System.Net; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.Listing; +using osu.Game.Overlays.Chat.ChannelList; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene + { + private ChatOverlayV2 chatOverlay; + private ChannelManager channelManager; + + private readonly APIUser testUser; + private readonly Channel testPMChannel; + private readonly Channel[] testChannels; + private Channel testChannel1 => testChannels[0]; + private Channel testChannel2 => testChannels[1]; + + public TestSceneChatOverlayV2() + { + testUser = new APIUser { Username = "test user", Id = 5071479 }; + testPMChannel = new Channel(testUser); + testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(ChannelManager), channelManager = new ChannelManager()), + }, + Children = new Drawable[] + { + channelManager, + chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both }, + }, + }; + }); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Setup request handler", () => + { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetUpdatesRequest getUpdates: + getUpdates.TriggerFailure(new WebException()); + return true; + + case JoinChannelRequest joinChannel: + joinChannel.TriggerSuccess(); + return true; + + case LeaveChannelRequest leaveChannel: + leaveChannel.TriggerSuccess(); + return true; + + case GetMessagesRequest getMessages: + getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel)); + return true; + + case GetUserRequest getUser: + if (getUser.Lookup == testUser.Username) + getUser.TriggerSuccess(testUser); + else + getUser.TriggerFailure(new WebException()); + return true; + + case PostMessageRequest postMessage: + postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000)) + { + Content = postMessage.Message.Content, + ChannelId = postMessage.Message.ChannelId, + Sender = postMessage.Message.Sender, + Timestamp = new DateTimeOffset(DateTime.Now), + }); + return true; + + default: + Logger.Log($"Unhandled Request Type: {req.GetType()}"); + return false; + } + }; + }); + + AddStep("Add test channels", () => + { + (channelManager.AvailableChannels as BindableList)?.AddRange(testChannels); + }); + } + + [Test] + public void TestShowHide() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden); + } + + [Test] + public void TestChannelSelection() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddAssert("Listing is hidden", () => listingVisibility == Visibility.Hidden); + AddAssert("Loading is hidden", () => loadingVisibility == Visibility.Hidden); + AddAssert("Current channel is correct", () => channelManager.CurrentChannel.Value == testChannel1); + AddAssert("DrawableChannel is correct", () => currentDrawableChannel.Channel == testChannel1); + } + + [Test] + public void TestSearchInListing() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); + AddStep("Search for 'number 2'", () => chatOverlay.ChildrenOfType().Single().Text = "number 2"); + AddUntilStep("Only channel 2 visibile", () => + { + IEnumerable listingItems = chatOverlay.ChildrenOfType() + .Where(item => item.IsPresent); + return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2; + }); + } + + [Test] + public void TestChannelCloseButton() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join PM and public channels", () => + { + channelManager.JoinChannel(testChannel1); + channelManager.JoinChannel(testPMChannel); + }); + AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); + AddStep("Click close button", () => + { + ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single(); + clickDrawable(closeButton); + }); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel)); + AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Click close button", () => + { + ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType().Single(); + clickDrawable(closeButton); + }); + AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1)); + } + + [Test] + public void TestChatCommand() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); + AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); + AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist")); + AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage); + // Make sure no unnecessary requests are made when the PM channel is already open. + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null); + AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); + AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); + AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + } + + [Test] + public void TestMultiplayerChannelIsNotShown() + { + Channel multiplayerChannel = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) + { + Name = "#mp_1", + Type = ChannelType.Multiplayer, + })); + AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); + AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType() + .Where(item => item.IsPresent) + .Select(item => item.Channel) + .Contains(multiplayerChannel)); + } + + [Test] + public void TestHighlightOnCurrentChannel() + { + Message message = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Send message in channel 1", () => + { + testChannel1.AddNewMessages(message = new Message + { + ChannelId = testChannel1.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + } + + [Test] + public void TestHighlightOnAnotherChannel() + { + Message message = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Send message in channel 2", () => + { + testChannel2.AddNewMessages(message = new Message + { + ChannelId = testChannel2.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); + AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2); + AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2); + } + + [Test] + public void TestHighlightOnLeftChannel() + { + Message message = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Send message in channel 2", () => + { + testChannel2.AddNewMessages(message = new Message + { + ChannelId = testChannel2.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2)); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); + AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2); + AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2); + } + + [Test] + public void TestHighlightWhileChatNeverOpen() + { + Message message = null; + + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Send message in channel 1", () => + { + testChannel1.AddNewMessages(message = new Message + { + ChannelId = testChannel1.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + } + + [Test] + public void TestHighlightWithNullChannel() + { + Message message = null; + + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Send message in channel 1", () => + { + testChannel1.AddNewMessages(message = new Message + { + ChannelId = testChannel1.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + } + + private Visibility listingVisibility => + chatOverlay.ChildrenOfType().Single().State.Value; + + private Visibility loadingVisibility => + chatOverlay.ChildrenOfType().Single().State.Value; + + private DrawableChannel currentDrawableChannel => + chatOverlay.ChildrenOfType>().Single().Child; + + private ChannelListItem getChannelListItem(Channel channel) => + chatOverlay.ChildrenOfType().Single(item => item.Channel == channel); + + private void clickDrawable(Drawable d) + { + InputManager.MoveMouseTo(d); + InputManager.Click(MouseButton.Left); + } + + private List createChannelMessages(Channel channel) + { + var message = new Message + { + ChannelId = channel.Id, + Content = $"Hello, this is a message in {channel.Name}", + Sender = testUser, + Timestamp = new DateTimeOffset(DateTime.Now), + }; + return new List { message }; + } + + private Channel createPublicChannel(int id) => new Channel + { + Id = id, + Name = $"#channel-{id}", + Topic = $"We talk about the number {id} here", + Type = ChannelType.Public, + }; + } +} diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 7c4a72559b..fa8fae29e5 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -25,14 +25,14 @@ namespace osu.Game.Overlays.Chat.ChannelList public event Action? OnRequestSelect; public event Action? OnRequestLeave; + public readonly Channel Channel; + public readonly BindableInt Mentions = new BindableInt(); public readonly BindableBool Unread = new BindableBool(); public readonly BindableBool SelectorActive = new BindableBool(); - private readonly Channel channel; - private Box hoverBox = null!; private Box selectBox = null!; private OsuSpriteText text = null!; @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat.ChannelList public ChannelListItem(Channel channel) { - this.channel = channel; + Channel = channel; } [BackgroundDependencyLoader] @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat.ChannelList { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = channel.Name, + Text = Channel.Name, Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), Colour = colourProvider.Light3, Margin = new MarginPadding { Bottom = 2 }, @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Chat.ChannelList Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding { Right = 3 }, - Action = () => OnRequestLeave?.Invoke(channel), + Action = () => OnRequestLeave?.Invoke(Channel), } } }, @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Chat.ChannelList }, }; - Action = () => OnRequestSelect?.Invoke(channel); + Action = () => OnRequestSelect?.Invoke(Channel); } protected override void LoadComplete() @@ -151,10 +151,10 @@ namespace osu.Game.Overlays.Chat.ChannelList private Drawable createIcon() { - if (channel.Type != ChannelType.PM) + if (Channel.Type != ChannelType.PM) return Drawable.Empty(); - return new UpdateableAvatar(channel.Users.First(), isInteractive: false) + return new UpdateableAvatar(Channel.Users.First(), isInteractive: false) { Size = new Vector2(20), Margin = new MarginPadding { Right = 5 }, @@ -167,7 +167,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private void updateSelectState() { - if (selectedChannel.Value == channel && !SelectorActive.Value) + if (selectedChannel.Value == Channel && !SelectorActive.Value) selectBox.FadeIn(300, Easing.OutQuint); else selectBox.FadeOut(200, Easing.OutQuint); diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs new file mode 100644 index 0000000000..9ba7608d89 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Chat +{ + public class ChatOverlayTopBar : Container + { + // IsHovered is used by overlay + public override bool HandlePositionalInput => true; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, TextureStore textures) + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get("Icons/Hexacons/messaging"), + Size = new Vector2(18), + }, + // Placeholder text + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "osu!chat", + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Bottom = 2f }, + }, + }, + }, + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs index 526cbcda87..1f0cbae7e2 100644 --- a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs +++ b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs @@ -25,11 +25,11 @@ namespace osu.Game.Overlays.Chat.Listing public event Action? OnRequestJoin; public event Action? OnRequestLeave; - public bool FilteringActive { get; set; } - public IEnumerable FilterTerms => new[] { channel.Name, channel.Topic ?? string.Empty }; - public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } + public readonly Channel Channel; - private readonly Channel channel; + public bool FilteringActive { get; set; } + public IEnumerable FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty }; + public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } private Box hoverBox = null!; private SpriteIcon checkbox = null!; @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat.Listing public ChannelListingItem(Channel channel) { - this.channel = channel; + Channel = channel; } [BackgroundDependencyLoader] @@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Chat.Listing { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = channel.Name, + Text = Channel.Name, Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold), Margin = new MarginPadding { Bottom = 2 }, }, @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Chat.Listing { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = channel.Topic, + Text = Channel.Topic, Font = OsuFont.Torus.With(size: text_size), Margin = new MarginPadding { Bottom = 2 }, }, @@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Chat.Listing { base.LoadComplete(); - channelJoined = channel.Joined.GetBoundCopy(); + channelJoined = Channel.Joined.GetBoundCopy(); channelJoined.BindValueChanged(change => { const double duration = 500; @@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Chat.Listing } }, true); - Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); + Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs new file mode 100644 index 0000000000..01b8ed2a45 --- /dev/null +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -0,0 +1,293 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.ChannelList; +using osu.Game.Overlays.Chat.Listing; + +namespace osu.Game.Overlays +{ + public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent + { + public string IconTexture => "Icons/Hexacons/messaging"; + public LocalisableString Title => ChatStrings.HeaderTitle; + public LocalisableString Description => ChatStrings.HeaderDescription; + + private ChatOverlayTopBar topBar = null!; + private ChannelList channelList = null!; + private LoadingLayer loading = null!; + private ChannelListing channelListing = null!; + private ChatTextBar textBar = null!; + private Container currentChannelContainer = null!; + + private Bindable? chatHeight; + private bool isDraggingTopBar; + private float dragStartChatHeight; + + private const int transition_length = 500; + private const float default_chat_height = 0.4f; + private const float top_bar_height = 40; + private const float side_bar_width = 190; + private const float chat_bar_height = 60; + + private readonly BindableBool selectorActive = new BindableBool(); + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Resolved] + private ChannelManager channelManager { get; set; } = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + [Cached] + private readonly Bindable currentChannel = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + // Width = 0.85f; // Matches OnlineOverlay + Height = default_chat_height; + RelativeSizeAxes = Axes.Both; + RelativePositionAxes = Axes.Both; + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + Masking = true; + CornerRadius = 7f; + Margin = new MarginPadding { Bottom = -10 }; + Padding = new MarginPadding { Bottom = 10 }; + + Children = new Drawable[] + { + topBar = new ChatOverlayTopBar + { + RelativeSizeAxes = Axes.X, + Height = top_bar_height, + }, + channelList = new ChannelList + { + RelativeSizeAxes = Axes.Y, + Width = side_bar_width, + Padding = new MarginPadding { Top = top_bar_height }, + SelectorActive = { BindTarget = selectorActive }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding + { + Top = top_bar_height, + Left = side_bar_width, + Bottom = chat_bar_height, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + loading = new LoadingLayer(true), + channelListing = new ChannelListing + { + RelativeSizeAxes = Axes.Both, + }, + }, + }, + textBar = new ChatTextBar + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Padding = new MarginPadding { Left = side_bar_width }, + ShowSearch = { BindTarget = selectorActive }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + loading.Show(); + + chatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy(); + chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); + + currentChannel.BindTo(channelManager.CurrentChannel); + channelManager.CurrentChannel.BindValueChanged(currentChannelChanged, true); + channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true); + channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true); + + channelList.OnRequestSelect += channel => + { + // Manually selecting a channel should dismiss the selector + selectorActive.Value = false; + channelManager.CurrentChannel.Value = channel; + }; + channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel); + + channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel); + channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel); + + textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms; + textBar.OnChatMessageCommitted += handleChatMessage; + + selectorActive.BindValueChanged(v => channelListing.State.Value = v.NewValue ? Visibility.Visible : Visibility.Hidden, true); + } + + /// + /// Highlights a certain message in the specified channel. + /// + /// The message to highlight. + /// The channel containing the message. + public void HighlightMessage(Message message, Channel channel) + { + Debug.Assert(channel.Id == message.ChannelId); + + if (currentChannel.Value?.Id != channel.Id) + { + if (!channel.Joined.Value) + channel = channelManager.JoinChannel(channel); + + channelManager.CurrentChannel.Value = channel; + } + + selectorActive.Value = false; + + channel.HighlightedMessage.Value = message; + + Show(); + } + + protected override bool OnDragStart(DragStartEvent e) + { + isDraggingTopBar = topBar.IsHovered; + + if (!isDraggingTopBar) + return base.OnDragStart(e); + + dragStartChatHeight = chatHeight!.Value; + return true; + } + + protected override void OnDrag(DragEvent e) + { + if (!isDraggingTopBar) + return; + + float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; + chatHeight!.Value = targetChatHeight; + } + + protected override void OnDragEnd(DragEndEvent e) + { + isDraggingTopBar = false; + base.OnDragEnd(e); + } + + protected override void PopIn() + { + this.MoveToY(0, transition_length, Easing.OutQuint); + this.FadeIn(transition_length, Easing.OutQuint); + base.PopIn(); + } + + protected override void PopOut() + { + this.MoveToY(Height, transition_length, Easing.InSine); + this.FadeOut(transition_length, Easing.InSine); + base.PopOut(); + } + + private void currentChannelChanged(ValueChangedEvent e) + { + Channel? newChannel = e.NewValue; + + loading.Show(); + + // Channel is null when leaving the currently selected channel + if (newChannel == null) + { + // Find another channel to switch to + newChannel = channelManager.JoinedChannels.FirstOrDefault(chan => chan != e.OldValue); + + if (newChannel == null) + selectorActive.Value = true; + else + currentChannel.Value = newChannel; + + return; + } + + LoadComponentAsync(new DrawableChannel(newChannel), loaded => + { + currentChannelContainer.Clear(); + currentChannelContainer.Add(loaded); + loading.Hide(); + }); + } + + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + IEnumerable joinedChannels = filterChannels(args.NewItems); + foreach (var channel in joinedChannels) + channelList.AddChannel(channel); + break; + + case NotifyCollectionChangedAction.Remove: + IEnumerable leftChannels = filterChannels(args.OldItems); + foreach (var channel in leftChannels) + channelList.RemoveChannel(channel); + break; + } + } + + private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + => channelListing.UpdateAvailableChannels(channelManager.AvailableChannels); + + private IEnumerable filterChannels(IList channels) + => channels.Cast().Where(c => c.Type == ChannelType.Public || c.Type == ChannelType.PM); + + private void handleChatMessage(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return; + + if (message[0] == '/') + channelManager.PostCommand(message.Substring(1)); + else + channelManager.PostMessage(message); + } + } +} + From 4bd1d091486feba7933432359758c531c40ec29b Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sun, 1 May 2022 12:20:11 +0100 Subject: [PATCH 02/20] Remove blank line --- osu.Game/Overlays/ChatOverlayV2.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 01b8ed2a45..d0b49d9ef3 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -290,4 +290,3 @@ namespace osu.Game.Overlays } } } - From bcce807311ae0a455ebf0771322c63ba9ed8c6d7 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sun, 1 May 2022 12:20:54 +0100 Subject: [PATCH 03/20] Fix chat command test as reference equality checks on PM channels doesn't seem to to work --- osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 312bda6460..c7bb6760c7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -185,16 +185,17 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); - AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); - AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + AddAssert("PM channel is selected", () => + channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist")); AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage); + // Make sure no unnecessary requests are made when the PM channel is already open. AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null); AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); - AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); - AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + AddAssert("PM channel is selected", () => + channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); } [Test] From e6f1ac6bec24e483f42e985a4ca43cf0454d99f5 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 20:08:33 +0100 Subject: [PATCH 04/20] Ensure "chatting in..." text is aligned with chat message --- osu.Game/Overlays/Chat/ChatTextBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index ef20149dac..15fa2d87db 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Chat private Container searchIconContainer = null!; private ChatTextBox chatTextBox = null!; - private const float chatting_text_width = 180; + private const float chatting_text_width = 240; private const float search_icon_width = 40; [BackgroundDependencyLoader] From 1473762e2567884d8e238e1ce3c713e76c0be42b Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 20:57:39 +0100 Subject: [PATCH 05/20] Don't wrap "chatting in.." text in `ChatTextBar` --- osu.Game/Overlays/Chat/ChatTextBar.cs | 31 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 15fa2d87db..316511d9a6 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -11,7 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osuTK; @@ -28,7 +29,8 @@ namespace osu.Game.Overlays.Chat [Resolved] private Bindable currentChannel { get; set; } = null!; - private OsuTextFlowContainer chattingTextContainer = null!; + private Container chattingTextContainer = null!; + private OsuSpriteText chattingText = null!; private Container searchIconContainer = null!; private ChatTextBox chatTextBox = null!; @@ -61,16 +63,19 @@ namespace osu.Game.Overlays.Chat { new Drawable[] { - chattingTextContainer = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 20)) + chattingTextContainer = new Container { - Masking = true, - Width = chatting_text_width, - Padding = new MarginPadding { Left = 10 }, RelativeSizeAxes = Axes.Y, - TextAnchor = Anchor.CentreRight, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = colourProvider.Background1, + Width = chatting_text_width, + Masking = true, + Child = chattingText = new OsuSpriteText + { + Font = OsuFont.Torus.With(size: 20), + Colour = colourProvider.Background1, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Truncate = true, + }, }, searchIconContainer = new Container { @@ -131,15 +136,15 @@ namespace osu.Game.Overlays.Chat switch (newChannel?.Type) { case ChannelType.Public: - chattingTextContainer.Text = $"chatting in {newChannel.Name}"; + chattingText.Text = $"chatting in {newChannel.Name}"; break; case ChannelType.PM: - chattingTextContainer.Text = $"chatting with {newChannel.Name}"; + chattingText.Text = $"chatting with {newChannel.Name}"; break; default: - chattingTextContainer.Text = string.Empty; + chattingText.Text = string.Empty; break; } }, true); From 7f8e00c1e3e8e6d66d4f9de132c0ffdddf9b37bf Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 21:22:47 +0100 Subject: [PATCH 06/20] Change "Add more channels" to sentence case in "ChannelList" --- osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs index 57ab7584b5..f9dab74eb1 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Chat.ChannelList { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = "Add More Channels", + Text = "Add more channels", Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), Colour = colourProvider.Light3, Margin = new MarginPadding { Bottom = 2 }, From a931b1ecc37a089edc4fcf55f93c94543cf26d08 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 22:32:25 +0100 Subject: [PATCH 07/20] Show selected channel text as white in `ChannelListItem` --- .../Chat/ChannelList/ChannelListItem.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index fa8fae29e5..e6a126f4d8 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -126,13 +126,9 @@ namespace osu.Game.Overlays.Chat.ChannelList { base.LoadComplete(); - selectedChannel.BindValueChanged(_ => updateSelectState(), true); - SelectorActive.BindValueChanged(_ => updateSelectState(), true); - - Unread.BindValueChanged(change => - { - text.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint); - }, true); + selectedChannel.BindValueChanged(_ => updateState(), true); + SelectorActive.BindValueChanged(_ => updateState(), true); + Unread.BindValueChanged(_ => updateState(), true); } protected override bool OnHover(HoverEvent e) @@ -165,12 +161,21 @@ namespace osu.Game.Overlays.Chat.ChannelList }; } - private void updateSelectState() + private void updateState() { - if (selectedChannel.Value == Channel && !SelectorActive.Value) + if (showSelected) selectBox.FadeIn(300, Easing.OutQuint); else selectBox.FadeOut(200, Easing.OutQuint); + + if (showUnread || showSelected) + text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); + else + text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint); } + + private bool showUnread => Unread.Value; + + private bool showSelected => selectedChannel.Value == Channel && !SelectorActive.Value; } } From 50aee8b665bcb9285fd6c8b099135f9aa56b32f4 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 3 May 2022 22:32:01 +0100 Subject: [PATCH 08/20] Ensure `ChannelListSelector` text also turns white when selected --- .../Overlays/Chat/ChannelList/ChannelListSelector.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs index f9dab74eb1..9cba93ffa5 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs @@ -21,6 +21,10 @@ namespace osu.Game.Overlays.Chat.ChannelList private Box hoverBox = null!; private Box selectBox = null!; + private OsuSpriteText text = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -46,7 +50,7 @@ namespace osu.Game.Overlays.Chat.ChannelList { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = 18, Right = 10 }, - Child = new OsuSpriteText + Child = text = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -68,9 +72,15 @@ namespace osu.Game.Overlays.Chat.ChannelList SelectorActive.BindValueChanged(selector => { if (selector.NewValue) + { + text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); selectBox.FadeIn(300, Easing.OutQuint); + } else + { + text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint); selectBox.FadeOut(200, Easing.OutQuint); + } }, true); Action = () => SelectorActive.Value = true; From c17edb2848e32dd803e88b6b95efe4c46d74ab2a Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 3 May 2022 22:32:51 +0100 Subject: [PATCH 09/20] Add padding to text in `ChatTextBar` to separate it from the textbox --- osu.Game/Overlays/Chat/ChatTextBar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 316511d9a6..3de7b67b0f 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -68,6 +68,7 @@ namespace osu.Game.Overlays.Chat RelativeSizeAxes = Axes.Y, Width = chatting_text_width, Masking = true, + Padding = new MarginPadding { Right = 5 }, Child = chattingText = new OsuSpriteText { Font = OsuFont.Torus.With(size: 20), From 60999e83e03647f71460c65ba57aa3d038d994ae Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 3 May 2022 22:33:36 +0100 Subject: [PATCH 10/20] Ensure `ChatTextBox` takes/leaves focus on chat overlay pop in/out --- osu.Game/Overlays/Chat/ChatTextBar.cs | 4 ++++ osu.Game/Overlays/ChatOverlayV2.cs | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 3de7b67b0f..0fa3613d38 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -26,6 +26,10 @@ namespace osu.Game.Overlays.Chat public event Action? OnSearchTermsChanged; + public void TextBoxTakeFocus() => chatTextBox.TakeFocus(); + + public void TextBoxKillFocus() => chatTextBox.KillFocus(); + [Resolved] private Bindable currentChannel { get; set; } = null!; diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index d0b49d9ef3..f6190bbe20 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -214,16 +214,22 @@ namespace osu.Game.Overlays protected override void PopIn() { + base.PopIn(); + this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); - base.PopIn(); + + textBar.TextBoxTakeFocus(); } protected override void PopOut() { + base.PopOut(); + this.MoveToY(Height, transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine); - base.PopOut(); + + textBar.TextBoxKillFocus(); } private void currentChannelChanged(ValueChangedEvent e) From ddab3c6d8084a0c19289c56f53bdb779481ec4b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 21:00:11 +0900 Subject: [PATCH 11/20] Tidy up state variables --- osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index e6a126f4d8..c5ac87f527 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -163,19 +163,17 @@ namespace osu.Game.Overlays.Chat.ChannelList private void updateState() { - if (showSelected) + bool selected = selectedChannel.Value == Channel && !SelectorActive.Value; + + if (selected) selectBox.FadeIn(300, Easing.OutQuint); else selectBox.FadeOut(200, Easing.OutQuint); - if (showUnread || showSelected) + if (Unread.Value || selected) text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); else text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint); } - - private bool showUnread => Unread.Value; - - private bool showSelected => selectedChannel.Value == Channel && !SelectorActive.Value; } } From 1a85e1267bc0ccf3700360f831b69f5e5cc26312 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Wed, 4 May 2022 14:43:40 +0100 Subject: [PATCH 12/20] Ensure focus is directed to `ChatTextBox` from `ChatOverlay` and add tests --- .../Visual/Online/TestSceneChatOverlayV2.cs | 27 ++++++++++++++++++- osu.Game/Overlays/ChatOverlayV2.cs | 8 ++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index c7bb6760c7..9d4de11c5a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); - AddStep("Search for 'number 2'", () => chatOverlay.ChildrenOfType().Single().Text = "number 2"); + AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2"); AddUntilStep("Only channel 2 visibile", () => { IEnumerable listingItems = chatOverlay.ChildrenOfType() @@ -325,6 +325,28 @@ namespace osu.Game.Tests.Visual.Online AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); } + [Test] + public void TextBoxRetainsFocus() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click selector", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click drawable channel", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null); + } + private Visibility listingVisibility => chatOverlay.ChildrenOfType().Single().State.Value; @@ -337,6 +359,9 @@ namespace osu.Game.Tests.Visual.Online private ChannelListItem getChannelListItem(Channel channel) => chatOverlay.ChildrenOfType().Single(item => item.Channel == channel); + private ChatTextBox chatOverlayTextBox => + chatOverlay.ChildrenOfType().Single(); + private void clickDrawable(Drawable d) { InputManager.MoveMouseTo(d); diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index f6190bbe20..658a28bfdb 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -218,8 +218,6 @@ namespace osu.Game.Overlays this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); - - textBar.TextBoxTakeFocus(); } protected override void PopOut() @@ -232,6 +230,12 @@ namespace osu.Game.Overlays textBar.TextBoxKillFocus(); } + protected override void OnFocus(FocusEvent e) + { + textBar.TextBoxTakeFocus(); + base.OnFocus(e); + } + private void currentChannelChanged(ValueChangedEvent e) { Channel? newChannel = e.NewValue; From e7205d8593ec1ab27528137eed48b2002d61cd85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:09:56 +0900 Subject: [PATCH 13/20] Reset all test data before each test method to avoid channels stuck in joined state --- .../Visual/Online/TestSceneChatOverlayV2.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 9d4de11c5a..b15a7afbf9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -31,22 +31,20 @@ namespace osu.Game.Tests.Visual.Online private ChatOverlayV2 chatOverlay; private ChannelManager channelManager; - private readonly APIUser testUser; - private readonly Channel testPMChannel; - private readonly Channel[] testChannels; + private APIUser testUser; + private Channel testPMChannel; + private Channel[] testChannels; + private Channel testChannel1 => testChannels[0]; private Channel testChannel2 => testChannels[1]; - public TestSceneChatOverlayV2() - { - testUser = new APIUser { Username = "test user", Id = 5071479 }; - testPMChannel = new Channel(testUser); - testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); - } - [SetUp] public void SetUp() => Schedule(() => { + testUser = new APIUser { Username = "test user", Id = 5071479 }; + testPMChannel = new Channel(testUser); + testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); + Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, From 25ea660b0b7f449bb74280a09b3f31db5b471d67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:13:48 +0900 Subject: [PATCH 14/20] Replace `HandlePositionalInput` override with simple hover effect --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 9ba7608d89..3a8cd1fb91 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -4,31 +4,35 @@ #nullable enable using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Chat { public class ChatOverlayTopBar : Container { - // IsHovered is used by overlay - public override bool HandlePositionalInput => true; + private Box background = null!; + + private Color4 backgroundColour; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) { Children = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, + Colour = backgroundColour = colourProvider.Background3, }, new GridContainer { @@ -63,5 +67,17 @@ namespace osu.Game.Overlays.Chat }, }; } + + protected override bool OnHover(HoverEvent e) + { + background.FadeColour(backgroundColour.Lighten(0.1f), 300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + background.FadeColour(backgroundColour, 300, Easing.OutQuint); + base.OnHoverLost(e); + } } } From 74505ba1666e821c905d27325bb86f7e0236aa2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:16:19 +0900 Subject: [PATCH 15/20] Remove `!` usage (also seems to fix height saving/loading) --- osu.Game/Overlays/ChatOverlayV2.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 658a28bfdb..ae4e160de5 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -39,7 +39,8 @@ namespace osu.Game.Overlays private ChatTextBar textBar = null!; private Container currentChannelContainer = null!; - private Bindable? chatHeight; + private readonly Bindable chatHeight = new Bindable(); + private bool isDraggingTopBar; private float dragStartChatHeight; @@ -137,7 +138,8 @@ namespace osu.Game.Overlays loading.Show(); - chatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy(); + config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight); + chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); currentChannel.BindTo(channelManager.CurrentChannel); @@ -193,7 +195,7 @@ namespace osu.Game.Overlays if (!isDraggingTopBar) return base.OnDragStart(e); - dragStartChatHeight = chatHeight!.Value; + dragStartChatHeight = chatHeight.Value; return true; } @@ -203,7 +205,7 @@ namespace osu.Game.Overlays return; float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; - chatHeight!.Value = targetChatHeight; + chatHeight.Value = targetChatHeight; } protected override void OnDragEnd(DragEndEvent e) From e54f5e2d9206f49b387beda75f70e9fcfddc5a4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:17:32 +0900 Subject: [PATCH 16/20] Adjust value change variables to avoid `e` usage --- osu.Game/Overlays/ChatOverlayV2.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index ae4e160de5..8a73eabed2 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -238,9 +238,9 @@ namespace osu.Game.Overlays base.OnFocus(e); } - private void currentChannelChanged(ValueChangedEvent e) + private void currentChannelChanged(ValueChangedEvent channel) { - Channel? newChannel = e.NewValue; + Channel? newChannel = channel.NewValue; loading.Show(); @@ -248,7 +248,7 @@ namespace osu.Game.Overlays if (newChannel == null) { // Find another channel to switch to - newChannel = channelManager.JoinedChannels.FirstOrDefault(chan => chan != e.OldValue); + newChannel = channelManager.JoinedChannels.FirstOrDefault(c => c != channel.OldValue); if (newChannel == null) selectorActive.Value = true; From 97221d2ef1ee5b88464fc2bdbbe7dc372d05c882 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:22:30 +0900 Subject: [PATCH 17/20] Tidy up initialisation --- osu.Game/Overlays/ChatOverlayV2.cs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 8a73eabed2..16da1f7c10 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -64,20 +64,27 @@ namespace osu.Game.Overlays [Cached] private readonly Bindable currentChannel = new Bindable(); + public ChatOverlayV2() + { + Height = default_chat_height; + + Masking = true; + + const float corner_radius = 7f; + + CornerRadius = corner_radius; + + // Hack to hide the bottom edge corner radius off-screen. + Margin = new MarginPadding { Bottom = -corner_radius }; + Padding = new MarginPadding { Bottom = corner_radius }; + + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + } + [BackgroundDependencyLoader] private void load() { - // Width = 0.85f; // Matches OnlineOverlay - Height = default_chat_height; - RelativeSizeAxes = Axes.Both; - RelativePositionAxes = Axes.Both; - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - Masking = true; - CornerRadius = 7f; - Margin = new MarginPadding { Bottom = -10 }; - Padding = new MarginPadding { Bottom = 10 }; - Children = new Drawable[] { topBar = new ChatOverlayTopBar From 5ea6f62951b93a9e558c59142e0275ce886d2459 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 14:20:33 +0100 Subject: [PATCH 18/20] Ensure `RelativePositionAxes` is set in BDL for animations to work --- osu.Game/Overlays/ChatOverlayV2.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 16da1f7c10..cab88136fc 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -85,6 +85,9 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { + // Required for the pop in/out animation + RelativePositionAxes = Axes.Both; + Children = new Drawable[] { topBar = new ChatOverlayTopBar From 9cb52f8879f839eb7ba64cd1b38a35d4bc0c69cb Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 14:21:26 +0100 Subject: [PATCH 19/20] Add tests for chat height saving/loading --- .../Visual/Online/TestSceneChatOverlayV2.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index b15a7afbf9..31ced4e8f7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -6,12 +6,14 @@ using System.Linq; using System.Collections.Generic; using System.Net; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -21,6 +23,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Listing; using osu.Game.Overlays.Chat.ChannelList; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Online @@ -38,6 +41,9 @@ namespace osu.Game.Tests.Visual.Online private Channel testChannel1 => testChannels[0]; private Channel testChannel2 => testChannels[1]; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + [SetUp] public void SetUp() => Schedule(() => { @@ -124,6 +130,28 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden); } + [Test] + public void TestChatHeight() + { + Bindable configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); + float newHeight = 0; + + AddStep("Set config chat height", () => configChatHeight.Value = 0.4f); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay uses config height", () => chatOverlay.Height == 0.4f); + AddStep("Drag overlay to new height", () => { + InputManager.MoveMouseTo(chatOverlayTopBar); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)); + InputManager.ReleaseButton(MouseButton.Left); + }); + AddStep("Store new height", () => newHeight = chatOverlay.Height); + AddAssert("Config height changed", () => configChatHeight.Value != 0.4f && configChatHeight.Value == newHeight); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight); + } + [Test] public void TestChannelSelection() { @@ -360,6 +388,9 @@ namespace osu.Game.Tests.Visual.Online private ChatTextBox chatOverlayTextBox => chatOverlay.ChildrenOfType().Single(); + private ChatOverlayTopBar chatOverlayTopBar => + chatOverlay.ChildrenOfType().Single(); + private void clickDrawable(Drawable d) { InputManager.MoveMouseTo(d); From 5657e7f11ec5d0e8551380efeb3e4998e6e55f3c Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 14:52:03 +0100 Subject: [PATCH 20/20] Fix chat height saving/loading test --- osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 31ced4e8f7..98574e5d53 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -133,13 +133,15 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChatHeight() { - Bindable configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); + Bindable configChatHeight = null; float newHeight = 0; + AddStep("Bind config chat height", () => configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy()); AddStep("Set config chat height", () => configChatHeight.Value = 0.4f); AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Overlay uses config height", () => chatOverlay.Height == 0.4f); - AddStep("Drag overlay to new height", () => { + AddStep("Drag overlay to new height", () => + { InputManager.MoveMouseTo(chatOverlayTopBar); InputManager.PressButton(MouseButton.Left); InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300));