diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index e4871f611e..c8ea692bb2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation typeof(DashboardOverlay), typeof(NewsOverlay), typeof(ChannelManager), - typeof(ChatOverlay), + typeof(ChatOverlayV2), typeof(SettingsOverlay), typeof(UserProfileOverlay), typeof(BeatmapSetOverlay), diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 2ce914ba3d..5d0116f80e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -86,9 +86,9 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestOverlaysAlwaysClosed() { - ChatOverlay chat = null; + ChatOverlayV2 chat = null; AddUntilStep("is at menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType().SingleOrDefault()) != null); + AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType().SingleOrDefault()) != null); AddStep("show chat", () => InputManager.Key(Key.F8)); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 6818147da4..a28de3be1e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Overlays; using osu.Game.Overlays.Chat; using osuTK.Graphics; @@ -22,12 +21,10 @@ namespace osu.Game.Tests.Visual.Online public class TestSceneChatLink : OsuTestScene { private readonly TestChatLineContainer textContainer; - private readonly DialogOverlay dialogOverlay; private Color4 linkColour; public TestSceneChatLink() { - Add(dialogOverlay = new DialogOverlay { Depth = float.MinValue }); Add(textContainer = new TestChatLineContainer { Padding = new MarginPadding { Left = 20, Right = 20 }, @@ -47,9 +44,6 @@ namespace osu.Game.Tests.Visual.Online availableChannels.Add(new Channel { Name = "#english" }); availableChannels.Add(new Channel { Name = "#japanese" }); Dependencies.Cache(chatManager); - - Dependencies.Cache(new ChatOverlay()); - Dependencies.CacheAs(dialogOverlay); } [SetUp] diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index e27db00003..0580d20171 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Testing; using osu.Framework.Utils; @@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online Children = new Drawable[] { channelManager, - chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both }, + chatOverlay = new TestChatOverlayV2(), }, }; }); @@ -417,6 +418,67 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); } + [Test] + public void TestKeyboardCloseAndRestoreChannel() + { + AddStep("Show overlay with channel 1", () => + { + channelManager.JoinChannel(testChannel1); + chatOverlay.Show(); + }); + AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); + + AddStep("Press document close keys", () => InputManager.Keys(PlatformAction.DocumentClose)); + AddAssert("Listing is visible", () => listingIsVisible); + + AddStep("Press tab restore keys", () => InputManager.Keys(PlatformAction.TabRestore)); + AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); + } + + [Test] + public void TestKeyboardNewChannel() + { + AddStep("Show overlay with channel 1", () => + { + channelManager.JoinChannel(testChannel1); + chatOverlay.Show(); + }); + AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); + + AddStep("Press tab new keys", () => InputManager.Keys(PlatformAction.TabNew)); + AddAssert("Listing is visible", () => listingIsVisible); + } + + [Test] + public void TestKeyboardNextChannel() + { + Channel pmChannel1 = createPrivateChannel(); + Channel pmChannel2 = createPrivateChannel(); + + AddStep("Show overlay with channels", () => + { + channelManager.JoinChannel(testChannel1); + channelManager.JoinChannel(testChannel2); + channelManager.JoinChannel(pmChannel1); + channelManager.JoinChannel(pmChannel2); + chatOverlay.Show(); + }); + + AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + AddAssert("PM Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == pmChannel1); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + AddAssert("PM Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == pmChannel2); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); + } + private bool listingIsVisible => chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible; @@ -467,6 +529,16 @@ namespace osu.Game.Tests.Visual.Online Type = ChannelType.Public, }; + private Channel createPrivateChannel() + { + int id = RNG.Next(0, 10000); + return new Channel(new APIUser + { + Id = id, + Username = $"test user {id}", + }); + } + private class TestChatOverlayV2 : ChatOverlayV2 { public bool SlowLoading { get; set; } diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 79f62a16e3..46f426597a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Online }; [Cached] - public ChatOverlay ChatOverlay { get; } = new ChatOverlay(); + public ChatOverlayV2 ChatOverlay { get; } = new ChatOverlayV2(); private readonly MessageNotifier messageNotifier = new MessageNotifier(); diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 20d555c16c..69e7dee1a5 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -46,7 +46,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); - SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); + SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlayV2.DEFAULT_HEIGHT, 0.2f, 1f); SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal); diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index ca6082e19b..cea3e321fa 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -27,7 +27,7 @@ namespace osu.Game.Online.Chat private INotificationOverlay notifications { get; set; } [Resolved] - private ChatOverlay chatOverlay { get; set; } + private ChatOverlayV2 chatOverlay { get; set; } [Resolved] private ChannelManager channelManager { get; set; } @@ -170,7 +170,7 @@ namespace osu.Game.Online.Chat public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) + private void load(OsuColour colours, ChatOverlayV2 chatOverlay, INotificationOverlay notificationOverlay) { IconBackground.Colour = colours.PurpleDark; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 785881d97a..402bd94f31 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -75,7 +75,7 @@ namespace osu.Game public Toolbar Toolbar; - private ChatOverlay chatOverlay; + private ChatOverlayV2 chatOverlay; private ChannelManager channelManager; @@ -848,7 +848,7 @@ namespace osu.Game loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); - loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); + loadComponentSingleFile(chatOverlay = new ChatOverlayV2(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true); loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true); diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index d1ceae604c..21b2251aca 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Linq; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -22,10 +23,13 @@ namespace osu.Game.Overlays.Chat.ChannelList public Action? OnRequestSelect; public Action? OnRequestLeave; + public IEnumerable Channels => publicChannelFlow.Channels.Concat(privateChannelFlow.Channels); + public readonly ChannelListing.ChannelListingChannel ChannelListingChannel = new ChannelListing.ChannelListingChannel(); private readonly Dictionary channelMap = new Dictionary(); + private OsuScrollContainer scroll = null!; private ChannelListItemFlow publicChannelFlow = null!; private ChannelListItemFlow privateChannelFlow = null!; private ChannelListItem selector = null!; @@ -40,7 +44,7 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background6, }, - new OsuScrollContainer + scroll = new OsuScrollContainer { Padding = new MarginPadding { Vertical = 7 }, RelativeSizeAxes = Axes.Both, @@ -53,12 +57,14 @@ namespace osu.Game.Overlays.Chat.ChannelList AutoSizeAxes = Axes.Y, Children = new Drawable[] { - publicChannelFlow = new ChannelListItemFlow("CHANNELS"), + new ChannelListLabel("CHANNELS"), + publicChannelFlow = new ChannelListItemFlow(), selector = new ChannelListItem(ChannelListingChannel) { Margin = new MarginPadding { Bottom = 10 }, }, - privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"), + new ChannelListLabel("DIRECT MESSAGES"), + privateChannelFlow = new ChannelListItemFlow(), }, }, }, @@ -101,6 +107,8 @@ namespace osu.Game.Overlays.Chat.ChannelList return channelMap[channel]; } + public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); + private ChannelListItemFlow getFlowForChannel(Channel channel) { switch (channel.Type) @@ -112,24 +120,29 @@ namespace osu.Game.Overlays.Chat.ChannelList return privateChannelFlow; default: - throw new ArgumentOutOfRangeException(); + return publicChannelFlow; } } - private class ChannelListItemFlow : FillFlowContainer + private class ChannelListLabel : OsuSpriteText { - public ChannelListItemFlow(string label) + public ChannelListLabel(string label) + { + Text = label; + Margin = new MarginPadding { Left = 18, Bottom = 5 }; + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold); + } + } + + private class ChannelListItemFlow : FillFlowContainer + { + public IEnumerable Channels => Children.Select(c => c.Channel); + + public ChannelListItemFlow() { Direction = FillDirection.Vertical; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - - Add(new OsuSpriteText - { - Text = label, - Margin = new MarginPadding { Left = 18, Bottom = 5 }, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), - }); } } } diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index ef479ea21b..f2ec007b19 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -3,7 +3,6 @@ #nullable enable -using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -13,6 +12,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -26,7 +27,7 @@ using osu.Game.Overlays.Chat.Listing; namespace osu.Game.Overlays { - public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent + public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler { public string IconTexture => "Icons/Hexacons/messaging"; public LocalisableString Title => ChatStrings.HeaderTitle; @@ -47,8 +48,9 @@ namespace osu.Game.Overlays private bool isDraggingTopBar; private float dragStartChatHeight; + public const float DEFAULT_HEIGHT = 0.4f; + 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; @@ -70,7 +72,7 @@ namespace osu.Game.Overlays public ChatOverlayV2() { - Height = default_chat_height; + Height = DEFAULT_HEIGHT; Masking = true; @@ -82,6 +84,7 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Bottom = -corner_radius }; Padding = new MarginPadding { Bottom = corner_radius }; + RelativeSizeAxes = Axes.Both; Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; } @@ -153,13 +156,15 @@ namespace osu.Game.Overlays chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); currentChannel.BindTo(channelManager.CurrentChannel); - currentChannel.BindValueChanged(currentChannelChanged, true); - joinedChannels.BindTo(channelManager.JoinedChannels); - joinedChannels.BindCollectionChanged(joinedChannelsChanged, true); - availableChannels.BindTo(channelManager.AvailableChannels); - availableChannels.BindCollectionChanged(availableChannelsChanged, true); + + Schedule(() => + { + currentChannel.BindValueChanged(currentChannelChanged, true); + joinedChannels.BindCollectionChanged(joinedChannelsChanged, true); + availableChannels.BindCollectionChanged(availableChannelsChanged, true); + }); channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel; channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel); @@ -193,6 +198,39 @@ namespace osu.Game.Overlays Show(); } + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case PlatformAction.TabNew: + currentChannel.Value = channelList.ChannelListingChannel; + return true; + + case PlatformAction.DocumentClose: + channelManager.LeaveChannel(currentChannel.Value); + return true; + + case PlatformAction.TabRestore: + channelManager.JoinLastClosedChannel(); + return true; + + case PlatformAction.DocumentPrevious: + cycleChannel(-1); + return true; + + case PlatformAction.DocumentNext: + cycleChannel(1); + return true; + + default: + return false; + } + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + protected override bool OnDragStart(DragStartEvent e) { isDraggingTopBar = topBar.IsHovered; @@ -294,6 +332,10 @@ namespace osu.Game.Overlays }); } } + + // Mark channel as read when channel switched + if (newChannel.Messages.Any()) + channelManager.MarkChannelAsRead(newChannel); } protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel); @@ -303,7 +345,7 @@ namespace osu.Game.Overlays switch (args.Action) { case NotifyCollectionChangedAction.Add: - IEnumerable newChannels = filterChannels(args.NewItems); + IEnumerable newChannels = args.NewItems.OfType().Where(isChatChannel); foreach (var channel in newChannels) channelList.AddChannel(channel); @@ -311,7 +353,7 @@ namespace osu.Game.Overlays break; case NotifyCollectionChangedAction.Remove: - IEnumerable leftChannels = filterChannels(args.OldItems); + IEnumerable leftChannels = args.OldItems.OfType().Where(isChatChannel); foreach (var channel in leftChannels) { @@ -333,9 +375,6 @@ namespace osu.Game.Overlays 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)) @@ -346,5 +385,36 @@ namespace osu.Game.Overlays else channelManager.PostMessage(message); } + + private void cycleChannel(int direction) + { + List overlayChannels = channelList.Channels.ToList(); + + if (overlayChannels.Count < 2) + return; + + int currentIndex = overlayChannels.IndexOf(currentChannel.Value); + + currentChannel.Value = overlayChannels[(currentIndex + direction + overlayChannels.Count) % overlayChannels.Count]; + + channelList.ScrollChannelIntoView(currentChannel.Value); + } + + /// + /// Whether a channel should be displayed in this overlay, based on its type. + /// + private static bool isChatChannel(Channel channel) + { + switch (channel.Type) + { + case ChannelType.Multiplayer: + case ChannelType.Spectator: + case ChannelType.Temporary: + return false; + + default: + return true; + } + } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs index e3dc5f818a..eafb453f75 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private UserProfileOverlay userOverlay { get; set; } [Resolved(CanBeNull = true)] - private ChatOverlay chatOverlay { get; set; } + private ChatOverlayV2 chatOverlay { get; set; } [Resolved] private IAPIProvider apiProvider { get; set; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index 2d3b33e9bc..20f405aae2 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader(true)] - private void load(ChatOverlay chat) + private void load(ChatOverlayV2 chat) { StateContainer = chat; }