diff --git a/osu.Game.Tests/Visual/TestCaseStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/TestCaseStandAloneChatDisplay.cs new file mode 100644 index 0000000000..16ce720ab1 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseStandAloneChatDisplay.cs @@ -0,0 +1,102 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Online.Chat; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseStandAloneChatDisplay : OsuTestCase + { + private readonly Channel testChannel = new Channel(); + + private readonly User admin = new User + { + Username = "HappyStick", + Id = 2, + Colour = "f2ca34" + }; + + private readonly User redUser = new User + { + Username = "BanchoBot", + Id = 3, + }; + + private readonly User blueUser = new User + { + Username = "Zallius", + Id = 4, + }; + + [Cached] + private ChannelManager channelManager = new ChannelManager(); + + private readonly StandAloneChatDisplay chatDisplay; + private readonly StandAloneChatDisplay chatDisplay2; + + public TestCaseStandAloneChatDisplay() + { + Add(channelManager); + + Add(chatDisplay = new StandAloneChatDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding(20), + Size = new Vector2(400, 80) + }); + + Add(chatDisplay2 = new StandAloneChatDisplay(true) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding(20), + Size = new Vector2(400, 150) + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + channelManager.CurrentChannel.Value = testChannel; + + chatDisplay.Channel.Value = testChannel; + chatDisplay2.Channel.Value = testChannel; + + AddStep("message from admin", () => testChannel.AddLocalEcho(new LocalEchoMessage + { + Sender = admin, + Content = "I am a wang!" + })); + + AddStep("message from team red", () => testChannel.AddLocalEcho(new LocalEchoMessage + { + Sender = redUser, + Content = "I am team red." + })); + + AddStep("message from team red", () => testChannel.AddLocalEcho(new LocalEchoMessage + { + Sender = redUser, + Content = "I plan to win!" + })); + + AddStep("message from team blue", () => testChannel.AddLocalEcho(new LocalEchoMessage + { + Sender = blueUser, + Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand." + })); + + AddStep("message from admin", () => testChannel.AddLocalEcho(new LocalEchoMessage + { + Sender = admin, + Content = "Okay okay, calm down guys. Let's do this!" + })); + } + } +} diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index a63af0f7a3..4241b47cd3 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -96,12 +96,14 @@ namespace osu.Game.Online.Chat /// /// The message text that is going to be posted /// Is true if the message is an action, e.g.: user is currently eating - public void PostMessage(string text, bool isAction = false) + /// An optional target channel. If null, will be used. + public void PostMessage(string text, bool isAction = false, Channel target = null) { - if (CurrentChannel.Value == null) - return; + if (target == null) + target = CurrentChannel.Value; - var currentChannel = CurrentChannel.Value; + if (target == null) + return; void dequeueAndRun() { @@ -113,7 +115,7 @@ namespace osu.Game.Online.Chat { if (!api.IsLoggedIn) { - currentChannel.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!")); + target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!")); return; } @@ -121,29 +123,29 @@ namespace osu.Game.Online.Chat { Sender = api.LocalUser.Value, Timestamp = DateTimeOffset.Now, - ChannelId = CurrentChannel.Value.Id, + ChannelId = target.Id, IsAction = isAction, Content = text }; - currentChannel.AddLocalEcho(message); + target.AddLocalEcho(message); // if this is a PM and the first message, we need to do a special request to create the PM channel - if (currentChannel.Type == ChannelType.PM && !currentChannel.Joined) + if (target.Type == ChannelType.PM && !target.Joined) { - var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(currentChannel.Users.First(), message); + var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(target.Users.First(), message); createNewPrivateMessageRequest.Success += createRes => { - currentChannel.Id = createRes.ChannelID; - currentChannel.ReplaceMessage(message, createRes.Message); + target.Id = createRes.ChannelID; + target.ReplaceMessage(message, createRes.Message); dequeueAndRun(); }; createNewPrivateMessageRequest.Failure += exception => { Logger.Error(exception, "Posting message failed."); - currentChannel.ReplaceMessage(message, null); + target.ReplaceMessage(message, null); dequeueAndRun(); }; @@ -155,14 +157,14 @@ namespace osu.Game.Online.Chat req.Success += m => { - currentChannel.ReplaceMessage(message, m); + target.ReplaceMessage(message, m); dequeueAndRun(); }; req.Failure += exception => { Logger.Error(exception, "Posting message failed."); - currentChannel.ReplaceMessage(message, null); + target.ReplaceMessage(message, null); dequeueAndRun(); }; @@ -178,9 +180,13 @@ namespace osu.Game.Online.Chat /// Posts a command locally. Commands like /help will result in a help message written in the current channel. /// /// the text containing the command identifier and command parameters. - public void PostCommand(string text) + /// An optional target channel. If null, will be used. + public void PostCommand(string text, Channel target = null) { - if (CurrentChannel.Value == null) + if (target == null) + target = CurrentChannel.Value; + + if (target == null) return; var parameters = text.Split(new[] { ' ' }, 2); @@ -192,7 +198,7 @@ namespace osu.Game.Online.Chat case "me": if (string.IsNullOrWhiteSpace(content)) { - CurrentChannel.Value.AddNewMessages(new ErrorMessage("Usage: /me [action]")); + target.AddNewMessages(new ErrorMessage("Usage: /me [action]")); break; } @@ -200,11 +206,11 @@ namespace osu.Game.Online.Chat break; case "help": - CurrentChannel.Value.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]")); + target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]")); break; default: - CurrentChannel.Value.AddNewMessages(new ErrorMessage($@"""/{command}"" is not supported! For a list of supported commands see /help")); + target.AddNewMessages(new ErrorMessage($@"""/{command}"" is not supported! For a list of supported commands see /help")); break; } } diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs new file mode 100644 index 0000000000..cb4bf9fdf8 --- /dev/null +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -0,0 +1,227 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Online.Chat +{ + /// + /// Display a chat channel in an insolated region. + /// + public class StandAloneChatDisplay : CompositeDrawable + { + public readonly Bindable Channel = new Bindable(); + + private readonly FillFlowContainer messagesFlow; + + private Channel lastChannel; + + private readonly FocusedTextBox textbox; + + protected ChannelManager ChannelManager; + + /// + /// Construct a new instance. + /// + /// Whether a textbox for posting new messages should be displayed. + public StandAloneChatDisplay(bool postingTextbox = false) + { + CornerRadius = 10; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Black, + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both + }, + messagesFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + LayoutEasing = Easing.Out, + LayoutDuration = 500, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Vertical + } + }; + + const float textbox_height = 30; + + if (postingTextbox) + { + messagesFlow.Y -= textbox_height; + AddInternal(textbox = new FocusedTextBox + { + RelativeSizeAxes = Axes.X, + Height = textbox_height, + PlaceholderText = "type your message", + OnCommit = postMessage, + ReleaseFocusOnCommit = false, + HoldFocus = true, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); + } + + Channel.BindValueChanged(channelChanged); + } + + [BackgroundDependencyLoader(true)] + private void load(ChannelManager manager) + { + if (ChannelManager == null) + ChannelManager = manager; + } + + private void postMessage(TextBox sender, bool newtext) + { + var text = textbox.Text.Trim(); + + if (string.IsNullOrWhiteSpace(text)) + return; + + if (text[0] == '/') + ChannelManager?.PostCommand(text.Substring(1)); + else + ChannelManager?.PostMessage(text); + + textbox.Text = string.Empty; + } + + public void Contract() + { + this.FadeIn(300); + this.MoveToY(0, 500, Easing.OutQuint); + } + + public void Expand() + { + this.FadeOut(200); + this.MoveToY(100, 500, Easing.In); + } + + protected virtual Drawable CreateMessage(Message message) + { + return new StandAloneMessage(message); + } + + private void channelChanged(Channel channel) + { + if (lastChannel != null) + lastChannel.NewMessagesArrived -= newMessages; + + lastChannel = channel; + messagesFlow.Clear(); + + if (channel == null) return; + + channel.NewMessagesArrived += newMessages; + } + + private void newMessages(IEnumerable messages) + { + var excessChildren = messagesFlow.Children.Count - 10; + if (excessChildren > 0) + foreach (var c in messagesFlow.Children.Take(excessChildren)) + c.Expire(); + + foreach (var message in messages) + { + var formatted = MessageFormatter.FormatMessage(message); + var drawable = CreateMessage(formatted); + drawable.Y = messagesFlow.Height; + messagesFlow.Add(drawable); + } + } + + protected class StandAloneMessage : CompositeDrawable + { + protected readonly Message Message; + protected OsuSpriteText SenderText; + protected Circle ColourBox; + + public StandAloneMessage(Message message) + { + Message = message; + } + + [BackgroundDependencyLoader] + private void load() + { + Margin = new MarginPadding(3); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Width = 0.2f, + Children = new Drawable[] + { + SenderText = new OsuSpriteText + { + Font = @"Exo2.0-Bold", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = Message.Sender.ToString() + } + } + }, + new Container + { + Size = new Vector2(8, OsuSpriteText.FONT_SIZE), + Margin = new MarginPadding { Horizontal = 3 }, + Children = new Drawable[] + { + ColourBox = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(8) + } + } + }, + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.5f, + Text = Message.DisplayContent + } + } + } + }; + + if (!string.IsNullOrEmpty(Message.Sender.Colour)) + SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour); + } + } + } +}