diff --git a/osu-framework b/osu-framework index b3f409ffb0..de1568254c 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit b3f409ffb027f1b16c639c7ce3bb9dc4f215f79b +Subproject commit de1568254c4c9a4ea540ccad94700c5c51f70dc2 diff --git a/osu.Desktop.VisualTests/Tests/TestCaseChatDisplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseChatDisplay.cs index d977e9cede..4e27788d67 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseChatDisplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseChatDisplay.cs @@ -17,6 +17,7 @@ using osu.Game.Online.Chat; using OpenTK; using osu.Framework.Allocation; using osu.Game.Online.Chat.Drawables; +using osu.Game.Overlays; namespace osu.Desktop.VisualTests.Tests { @@ -25,119 +26,16 @@ namespace osu.Desktop.VisualTests.Tests private ScheduledDelegate messageRequest; public override string Name => @"Chat"; - public override string Description => @"Testing API polling"; - - FlowContainer flow; - - private Scheduler scheduler = new Scheduler(); - - private APIAccess api; - - private ChannelDisplay channelDisplay; - - [BackgroundDependencyLoader] - private void load(APIAccess api) - { - this.api = api; - } + public override string Description => @"Testing chat api and overlay"; public override void Reset() { base.Reset(); - if (api.State != APIState.Online) - api.OnStateChange += delegate { initializeChannels(); }; - else - initializeChannels(); - } - - protected override void Update() - { - scheduler.Update(); - base.Update(); - } - - private long? lastMessageId; - - List careChannels; - - private void initializeChannels() - { - careChannels = new List(); - - if (api.State != APIState.Online) - return; - - Add(flow = new FlowContainer + Add(new ChatOverlay() { - RelativeSizeAxes = Axes.Both, - Direction = FlowDirections.Vertical + State = Visibility.Visible }); - - SpriteText loading; - Add(loading = new SpriteText - { - Text = @"Loading available channels...", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - TextSize = 40, - }); - - messageRequest?.Cancel(); - - ListChannelsRequest req = new ListChannelsRequest(); - req.Success += delegate (List channels) - { - Scheduler.Add(delegate - { - loading.FadeOut(100); - }); - - addChannel(channels.Find(c => c.Name == @"#osu")); - addChannel(channels.Find(c => c.Name == @"#lobby")); - addChannel(channels.Find(c => c.Name == @"#english")); - - messageRequest = scheduler.AddDelayed(() => FetchNewMessages(api), 1000, true); - }; - api.Queue(req); - } - - private void addChannel(Channel channel) - { - flow.Add(channelDisplay = new ChannelDisplay(channel) - { - Size = new Vector2(1, 0.3f) - }); - - careChannels.Add(channel); - } - - GetMessagesRequest fetchReq; - - public void FetchNewMessages(APIAccess api) - { - if (fetchReq != null) return; - - fetchReq = new GetMessagesRequest(careChannels, lastMessageId); - fetchReq.Success += delegate (List messages) - { - foreach (Message m in messages) - { - careChannels.Find(c => c.Id == m.ChannelId).AddNewMessages(m); - } - - lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId; - - Debug.Write("success!"); - fetchReq = null; - }; - fetchReq.Failure += delegate - { - Debug.Write("failure!"); - fetchReq = null; - }; - - api.Queue(fetchReq); } } } diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs new file mode 100644 index 0000000000..f657ffe330 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -0,0 +1,49 @@ +using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Input; +using System; +using System.Linq; + +namespace osu.Game.Graphics.UserInterface +{ + public class FocusedTextBox : OsuTextBox + { + protected override Color4 BackgroundUnfocused => new Color4(10, 10, 10, 255); + protected override Color4 BackgroundFocused => new Color4(10, 10, 10, 255); + + public Action Exit; + + private bool focus; + public bool HoldFocus + { + get { return focus; } + set + { + focus = value; + if (!focus) + TriggerFocusLost(); + } + } + + protected override bool OnFocus(InputState state) + { + var result = base.OnFocus(state); + BorderThickness = 0; + return result; + } + + protected override void OnFocusLost(InputState state) + { + if (state.Keyboard.Keys.Any(key => key == Key.Escape)) + { + if (Text.Length > 0) + Text = string.Empty; + else + Exit?.Invoke(); + } + base.OnFocusLost(state); + } + + public override bool RequestingFocus => HoldFocus; + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 0e551380ed..d7bc245027 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; using OpenTK.Graphics; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index e67bfabc2e..d895b93336 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -3,12 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using Newtonsoft.Json; -using osu.Framework.Configuration; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; namespace osu.Game.Online.Chat { @@ -30,16 +25,16 @@ namespace osu.Game.Online.Chat //internal bool Joined; - public const int MAX_HISTORY = 100; + public const int MAX_HISTORY = 300; [JsonConstructor] public Channel() { } - public event Action NewMessagesArrived; + public event Action> NewMessagesArrived; - public void AddNewMessages(params Message[] messages) + public void AddNewMessages(IEnumerable messages) { Messages.AddRange(messages); purgeOldMessages(); diff --git a/osu.Game/Online/Chat/Drawables/ChatLine.cs b/osu.Game/Online/Chat/Drawables/ChatLine.cs index 21c6659e78..7c6b079eb0 100644 --- a/osu.Game/Online/Chat/Drawables/ChatLine.cs +++ b/osu.Game/Online/Chat/Drawables/ChatLine.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; @@ -15,6 +17,50 @@ namespace osu.Game.Online.Chat.Drawables { public readonly Message Message; + private static readonly Color4[] username_colours = { + OsuColour.FromHex("588c7e"), + OsuColour.FromHex("b2a367"), + OsuColour.FromHex("c98f65"), + OsuColour.FromHex("bc5151"), + OsuColour.FromHex("5c8bd6"), + OsuColour.FromHex("7f6ab7"), + OsuColour.FromHex("a368ad"), + OsuColour.FromHex("aa6880"), + + OsuColour.FromHex("6fad9b"), + OsuColour.FromHex("f2e394"), + OsuColour.FromHex("f2ae72"), + OsuColour.FromHex("f98f8a"), + OsuColour.FromHex("7daef4"), + OsuColour.FromHex("a691f2"), + OsuColour.FromHex("c894d3"), + OsuColour.FromHex("d895b0"), + + OsuColour.FromHex("53c4a1"), + OsuColour.FromHex("eace5c"), + OsuColour.FromHex("ea8c47"), + OsuColour.FromHex("fc4f4f"), + OsuColour.FromHex("3d94ea"), + OsuColour.FromHex("7760ea"), + OsuColour.FromHex("af52c6"), + OsuColour.FromHex("e25696"), + + OsuColour.FromHex("677c66"), + OsuColour.FromHex("9b8732"), + OsuColour.FromHex("8c5129"), + OsuColour.FromHex("8c3030"), + OsuColour.FromHex("1f5d91"), + OsuColour.FromHex("4335a5"), + OsuColour.FromHex("812a96"), + OsuColour.FromHex("992861"), + }; + + private Color4 getUsernameColour(Message message) + { + //todo: use User instead of Message when user_id is correctly populated. + return username_colours[message.UserId % username_colours.Length]; + } + const float padding = 200; const float text_size = 20; @@ -25,6 +71,8 @@ namespace osu.Game.Online.Chat.Drawables RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Left = 15, Right = 15 }; + Children = new Drawable[] { new Container @@ -34,13 +82,19 @@ namespace osu.Game.Online.Chat.Drawables { new OsuSpriteText { - Text = Message.Timestamp.LocalDateTime.ToLongTimeString(), - TextSize = text_size, - Colour = Color4.Gray + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = @"Exo2.0-SemiBold", + Text = $@"{Message.Timestamp.LocalDateTime:hh:mm:ss}", + FixedWidth = true, + TextSize = text_size * 0.75f, + Alpha = 0.4f, }, new OsuSpriteText { - Text = Message.User.Name, + Font = @"Exo2.0-BoldItalic", + Text = $@"{Message.User.Name}:", + Colour = getUsernameColour(Message), TextSize = text_size, Origin = Anchor.TopRight, Anchor = Anchor.TopRight, @@ -51,7 +105,7 @@ namespace osu.Game.Online.Chat.Drawables { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = padding + 10 }, + Padding = new MarginPadding { Left = padding + 15 }, Children = new Drawable[] { new OsuSpriteText diff --git a/osu.Game/Online/Chat/Drawables/ChannelDisplay.cs b/osu.Game/Online/Chat/Drawables/DrawableChannel.cs similarity index 54% rename from osu.Game/Online/Chat/Drawables/ChannelDisplay.cs rename to osu.Game/Online/Chat/Drawables/DrawableChannel.cs index cc60706831..5b8d034e9d 100644 --- a/osu.Game/Online/Chat/Drawables/ChannelDisplay.cs +++ b/osu.Game/Online/Chat/Drawables/DrawableChannel.cs @@ -4,25 +4,25 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Primitives; +using osu.Framework.MathUtils; +using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using OpenTK; namespace osu.Game.Online.Chat.Drawables { - public class ChannelDisplay : Container + public class DrawableChannel : Container { private readonly Channel channel; private FlowContainer flow; + private ScrollContainer scroll; - public ChannelDisplay(Channel channel) + public DrawableChannel(Channel channel) { this.channel = channel; - newMessages(channel.Messages); - channel.NewMessagesArrived += newMessages; RelativeSizeAxes = Axes.Both; @@ -36,7 +36,7 @@ namespace osu.Game.Online.Chat.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre }, - new ScrollContainer + scroll = new ScrollContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -46,37 +46,54 @@ namespace osu.Game.Online.Chat.Drawables Direction = FlowDirections.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(1, 1) + Padding = new MarginPadding { Left = 20, Right = 20 } } } } }; + + channel.NewMessagesArrived += newMessagesArrived; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + newMessagesArrived(channel.Messages); + scrollToEnd(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - channel.NewMessagesArrived -= newMessages; + channel.NewMessagesArrived -= newMessagesArrived; } - [BackgroundDependencyLoader] - private void load() - { - newMessages(channel.Messages); - } - - private void newMessages(IEnumerable newMessages) + private void newMessagesArrived(IEnumerable newMessages) { if (!IsLoaded) return; var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); + if (scroll.IsScrolledToEnd(10) || !flow.Children.Any()) + scrollToEnd(); + //up to last Channel.MAX_HISTORY messages foreach (Message m in displayMessages) - flow.Add(new ChatLine(m)); + { + var d = new ChatLine(m); + flow.Add(d); + } - while (flow.Children.Count() > Channel.MAX_HISTORY) - flow.Remove(flow.Children.First()); + while (flow.Children.Count(c => c.LifetimeEnd == double.MaxValue) > Channel.MAX_HISTORY) + { + var d = flow.Children.First(c => c.LifetimeEnd == double.MaxValue); + if (!scroll.IsScrolledToEnd(10)) + scroll.OffsetScrollPosition(-d.DrawHeight); + d.Expire(); + } } + + private void scrollToEnd() => Scheduler.AddDelayed(() => scroll.ScrollToEnd(), 50); } } \ No newline at end of file diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index d412650176..3081653c34 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -11,6 +11,7 @@ namespace osu.Game.Online.Chat [JsonProperty(@"message_id")] public long Id; + //todo: this should be inside sender. [JsonProperty(@"user_id")] public int UserId; diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index f99936763f..e0e43fbe64 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenTK; -using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,12 +18,19 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; using osu.Game.Online.Chat.Drawables; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.UserInterface; +using OpenTK.Graphics; +using osu.Framework.Input; namespace osu.Game.Overlays { - public class ChatOverlay : OverlayContainer, IOnlineComponent + public class ChatOverlay : FocusedOverlayContainer, IOnlineComponent { - private ChannelDisplay channelDisplay; + const float textbox_height = 40; + + private DrawableChannel channelDisplay; private ScheduledDelegate messageRequest; @@ -32,6 +38,8 @@ namespace osu.Game.Overlays protected override Container Content => content; + private FocusedTextBox inputTextBox; + private APIAccess api; public ChatOverlay() @@ -47,15 +55,65 @@ namespace osu.Game.Overlays { Depth = float.MaxValue, RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.1f).Opacity(0.4f), + Colour = Color4.Black, + Alpha = 0.9f, }, content = new Container { RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 5, Bottom = textbox_height + 5 }, + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = textbox_height, + Padding = new MarginPadding(5), + Children = new Drawable[] + { + inputTextBox = new FocusedTextBox + { + RelativeSizeAxes = Axes.Both, + Height = 1, + PlaceholderText = "type your message", + Exit = () => State = Visibility.Hidden, + OnCommit = postMessage, + HoldFocus = true, + } + } } }); } + protected override bool OnFocus(InputState state) + { + //this is necessary as inputTextBox is masked away and therefore can't get focus :( + inputTextBox.TriggerFocus(); + return false; + } + + private void postMessage(TextBox sender, bool newText) + { + var postText = sender.Text; + + if (!string.IsNullOrEmpty(postText)) + { + //todo: actually send to server + careChannels.FirstOrDefault()?.AddNewMessages(new[] + { + new Message + { + User = api.LocalUser.Value, + Timestamp = DateTimeOffset.Now, + Content = postText + } + }); + } + + sender.Text = string.Empty; + } + [BackgroundDependencyLoader] private void load(APIAccess api) { @@ -69,7 +127,7 @@ namespace osu.Game.Overlays private void addChannel(Channel channel) { - Add(channelDisplay = new ChannelDisplay(channel)); + Add(channelDisplay = new DrawableChannel(channel)); careChannels.Add(channel); } @@ -82,10 +140,11 @@ namespace osu.Game.Overlays fetchReq = new GetMessagesRequest(careChannels, lastMessageId); fetchReq.Success += delegate (List messages) { - foreach (Message m in messages) - { - careChannels.Find(c => c.Id == m.ChannelId).AddNewMessages(m); - } + var ids = messages.Select(m => m.ChannelId).Distinct(); + + //batch messages per channel. + foreach (var id in ids) + careChannels.Find(c => c.Id == id)?.AddNewMessages(messages.Where(m => m.ChannelId == id)); lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId; @@ -151,6 +210,8 @@ namespace osu.Game.Overlays ListChannelsRequest req = new ListChannelsRequest(); req.Success += delegate (List channels) { + Debug.Assert(careChannels.Count == 0); + Scheduler.Add(delegate { loading.FadeOut(100); diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 975a3f9ca3..23a61459ea 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -47,18 +47,19 @@ namespace osu.Game.Screens.Select Direction = FlowDirections.Vertical, Children = new Drawable[] { - searchTextBox = new SearchTextBox { RelativeSizeAxes = Axes.X }, + searchTextBox = new SearchTextBox { + RelativeSizeAxes = Axes.X, + OnChange = (TextBox sender, bool newText) => + { + if (newText) + FilterChanged?.Invoke(); + }, + Exit = () => Exit?.Invoke(), + }, new GroupSortTabs() } } }; - - searchTextBox.OnChange += (TextBox sender, bool newText) => - { - if (newText) - FilterChanged?.Invoke(); - }; - searchTextBox.Exit = () => Exit?.Invoke(); } public void Deactivate() diff --git a/osu.Game/Screens/Select/SearchTextBox.cs b/osu.Game/Screens/Select/SearchTextBox.cs index 4cd5353712..aeb9db75db 100644 --- a/osu.Game/Screens/Select/SearchTextBox.cs +++ b/osu.Game/Screens/Select/SearchTextBox.cs @@ -16,26 +16,8 @@ namespace osu.Game.Screens.Select /// /// A textbox which holds focus eagerly. /// - public class SearchTextBox : OsuTextBox + public class SearchTextBox : FocusedTextBox { - protected override Color4 BackgroundUnfocused => new Color4(10, 10, 10, 255); - protected override Color4 BackgroundFocused => new Color4(10, 10, 10, 255); - public Action Exit; - - private bool focus; - public bool HoldFocus - { - get { return focus; } - set - { - focus = value; - if (!focus) - TriggerFocusLost(); - } - } - - public override bool RequestingFocus => HoldFocus; - public SearchTextBox() { Height = 35; @@ -53,25 +35,6 @@ namespace osu.Game.Screens.Select PlaceholderText = "type to search"; } - protected override bool OnFocus(InputState state) - { - var result = base.OnFocus(state); - BorderThickness = 0; - return result; - } - - protected override void OnFocusLost(InputState state) - { - if (state.Keyboard.Keys.Any(key => key == Key.Escape)) - { - if (Text.Length > 0) - Text = string.Empty; - else - Exit?.Invoke(); - } - base.OnFocusLost(state); - } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { if (HandlePendingText(state)) return true; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 39e2bfcd03..790cd0d348 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -71,6 +71,7 @@ + @@ -190,7 +191,7 @@ - +