From 01bea3bada24b6540631dfe7eadb695c93dc33a6 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 10:56:48 +0100 Subject: [PATCH 01/13] Re-implemented message formatting (mostly taken from osu-stable code) --- osu.Game/Online/Chat/MessageFormatter.cs | 169 +++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 osu.Game/Online/Chat/MessageFormatter.cs diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs new file mode 100644 index 0000000000..df77b5d058 --- /dev/null +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace osu.Game.Online.Chat +{ + public static class MessageFormatter + { + // [[Performance Points]] -> wiki:Performance Points (https://osu.ppy.sh/wiki/Performance_Points) + private static Regex wikiRegex = new Regex(@"\[\[([^\]]+)\]\]"); + + // (test)[https://osu.ppy.sh/b/1234] -> test (https://osu.ppy.sh/b/1234) + private static Regex oldLinkRegex = new Regex(@"\(([^\)]*)\)\[([a-z]+://[^ ]+)\]"); + + // [https://osu.ppy.sh/b/1234 Beatmap [Hard] (poop)] -> Beatmap [hard] (poop) (https://osu.ppy.sh/b/1234) + private static Regex newLinkRegex = new Regex(@"\[([a-z]+://[^ ]+) ([^\[\]]*(((?\[)[^\[\]]*)+((?\])[^\[\]]*)+)*(?(open)(?!)))\]"); + + // advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used + // This is in the format (, [optional]): + // http[s]://.[:port][/path][?query][#fragment] + private static Regex advancedLinkRegex = new Regex(@"(?\([^)]*)?" + + @"(?https?:\/\/" + + @"(?(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z][a-z0-9-]*[a-z0-9]" + // domain, TLD + @"(?::\d+)?)" + // port + @"(?(?:(?:\/+(?:[a-z0-9$_\.\+!\*\',;:\(\)@&~=-]|%[0-9a-f]{2})*)*" + // path + @"(?:\?(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?" + // query + @"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)", // fragment + RegexOptions.IgnoreCase); + + // 00:00:000 (1,2,3) - test + private static Regex timeRegex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*"); + + // #osu + private static Regex channelRegex = new Regex(@"#[a-zA-Z]+[a-zA-Z0-9]+"); + + // Unicode emojis + private static Regex emojiRegex = new Regex(@"(\uD83D[\uDC00-\uDE4F])"); + + private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0) + { + int captureOffset = 0; + foreach (Match m in regex.Matches(result.Text, startIndex)) + { + var index = m.Index - captureOffset; + + var displayText = string.Format(display, + m.Groups[0], + m.Groups.Count > 1 ? m.Groups[1].Value : "", + m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim(); + + var linkText = string.Format(link, + m.Groups[0], + m.Groups.Count > 1 ? m.Groups[1].Value : "", + m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim(); + + var testText = string.Format(link, m.Groups).Trim(); + + if (displayText.Length == 0 || linkText.Length == 0) continue; + + // Check for encapsulated links + if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || index <= l.Index && index + m.Length >= l.Index + l.Length) == null) + { + result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText); + + //since we just changed the line display text, offset any already processed links. + result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0); + + result.Links.Add(new Link(linkText, index, displayText.Length)); + + //adjust the offset for processing the current matches group. + captureOffset += (m.Length - displayText.Length); + } + } + } + + private static void handleAdvanced(Regex regex, MessageFormatterResult result, int startIndex = 0) + { + foreach (Match m in regex.Matches(result.Text, startIndex)) + { + var index = m.Index; + var prefix = m.Groups["paren"].Value; + var link = m.Groups["link"].Value; + var indexLength = link.Length; + + if (!String.IsNullOrEmpty(prefix)) + { + index += prefix.Length; + if (link.EndsWith(")")) + { + indexLength = indexLength - 1; + link = link.Remove(link.Length - 1); + } + } + + result.Links.Add(new Link(link, index, indexLength)); + } + } + + private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) + { + var result = new MessageFormatterResult(toFormat); + + // handle the [link display] format + handleMatches(newLinkRegex, "{2}", "{1}", result, startIndex); + + // handle the ()[] link format + handleMatches(oldLinkRegex, "{1}", "{2}", result, startIndex); + + // handle wiki links + handleMatches(wikiRegex, "wiki:{1}", "https://osu.ppy.sh/wiki/{1}", result, startIndex); + + // handle bare links + handleAdvanced(advancedLinkRegex, result, startIndex); + + // handle editor times + handleMatches(timeRegex, "{0}", "osu://edit/{0}", result, startIndex); + + // handle channels + handleMatches(channelRegex, "{0}", "osu://chan/{0}", result, startIndex); + + var empty = ""; + while (space-- > 0) + empty += "\0"; + + handleMatches(emojiRegex, empty, "{0}", result, startIndex); + + return result; + } + + public static FormattedMessage FormatMessage(Message inputMessage) + { + var result = format(inputMessage.Content); + + FormattedMessage formatted = inputMessage as FormattedMessage; + formatted.Content = result.Text; + formatted.Links = result.Links; + return formatted; + } + + public class MessageFormatterResult + { + public List Links = new List(); + public string Text; + public string OriginalText; + + public MessageFormatterResult(string text) + { + OriginalText = Text = text; + } + } + + public class Link + { + public string Url; + public int Index; + public int Length; + + public Link(string url, int startIndex, int length) + { + Url = url; + Index = startIndex; + Length = length; + } + } + } +} From f5f287bed57949df1a429e2f4889e613c27ca353 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 20:25:02 +0100 Subject: [PATCH 02/13] Rolled back the idea that there should be a separate class for formatted messages --- osu.Game/Online/Chat/Message.cs | 4 ++++ osu.Game/Online/Chat/MessageFormatter.cs | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 79b5c4fc1a..355abfda59 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections; +using System.Collections.Generic; using System.ComponentModel; using Newtonsoft.Json; using osu.Game.Users; @@ -40,6 +42,8 @@ namespace osu.Game.Online.Chat { } + public List Links; + public Message(long? id) { Id = id; diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index df77b5d058..a712cb1f2b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -1,4 +1,7 @@ -using System; +// 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 System.Linq; using System.Text; @@ -56,8 +59,6 @@ namespace osu.Game.Online.Chat m.Groups.Count > 1 ? m.Groups[1].Value : "", m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim(); - var testText = string.Format(link, m.Groups).Trim(); - if (displayText.Length == 0 || linkText.Length == 0) continue; // Check for encapsulated links @@ -130,11 +131,11 @@ namespace osu.Game.Online.Chat return result; } - public static FormattedMessage FormatMessage(Message inputMessage) + public static Message FormatMessage(Message inputMessage) { var result = format(inputMessage.Content); + var formatted = inputMessage; - FormattedMessage formatted = inputMessage as FormattedMessage; formatted.Content = result.Text; formatted.Links = result.Links; return formatted; From 1f1c7dd70fb12bf2b2d80bb8956d4a9935f08537 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 20:26:51 +0100 Subject: [PATCH 03/13] Moved LinkFlowContainer out of ProfileHeader to make it available for other uses too (e.g. chat) and renamed it to LinkTextFlowContainer bc it can contain both links and text, not only one --- .../Containers/OsuLinkTextFlowContainer.cs | 44 +++++++++++++ osu.Game/Overlays/Profile/ProfileHeader.cs | 61 +++---------------- 2 files changed, 54 insertions(+), 51 deletions(-) create mode 100644 osu.Game/Graphics/Containers/OsuLinkTextFlowContainer.cs diff --git a/osu.Game/Graphics/Containers/OsuLinkTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuLinkTextFlowContainer.cs new file mode 100644 index 0000000000..b5490d42f9 --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuLinkTextFlowContainer.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Graphics.Sprites; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.Containers +{ + public class OsuLinkTextFlowContainer : OsuLinkTextFlowContainer + { + public OsuLinkTextFlowContainer(Action defaultCreationParameters = null) + : base(defaultCreationParameters) + { + } + } + + public class OsuLinkTextFlowContainer : OsuTextFlowContainer + where T : OsuLinkSpriteText, new() + { + public override bool HandleInput => true; + + public OsuLinkTextFlowContainer(Action defaultCreationParameters = null) : base(defaultCreationParameters) + { + } + + protected override SpriteText CreateSpriteText() => new T(); + + public void AddLink(string text, string url, Action creationParameters = null) + { + AddText(text, link => + { + ((T)link).Url = url; + creationParameters?.Invoke(link); + }); + } + } +} diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index c7bc5c1d93..af3e97db27 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile public class ProfileHeader : Container { private readonly OsuTextFlowContainer infoTextLeft; - private readonly LinkFlowContainer infoTextRight; + private readonly OsuLinkTextFlowContainer infoTextRight; private readonly FillFlowContainer scoreText, scoreNumberText; private readonly Container coverContainer, chartContainer, supporterTag; @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Profile } } }, - new LinkFlowContainer.ProfileLink(user) + new ProfileLink(user) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -160,7 +160,7 @@ namespace osu.Game.Overlays.Profile ParagraphSpacing = 0.8f, LineSpacing = 0.2f }, - infoTextRight = new LinkFlowContainer(t => + infoTextRight = new OsuLinkTextFlowContainer(t => { t.TextSize = 14; t.Font = @"Exo2.0-RegularItalic"; @@ -488,57 +488,16 @@ namespace osu.Game.Overlays.Profile } } - private class LinkFlowContainer : OsuTextFlowContainer + private class ProfileLink : OsuLinkSpriteText, IHasTooltip { - public override bool HandleInput => true; + public string TooltipText => "View Profile in Browser"; - public LinkFlowContainer(Action defaultCreationParameters = null) : base(defaultCreationParameters) + public ProfileLink(User user) { - } - - protected override SpriteText CreateSpriteText() => new LinkText(); - - public void AddLink(string text, string url) => AddText(text, link => ((LinkText)link).Url = url); - - public class LinkText : OsuSpriteText - { - private readonly OsuHoverContainer content; - - public override bool HandleInput => content.Action != null; - - protected override Container Content => content ?? (Container)this; - - protected override IEnumerable FlowingChildren => Children; - - public string Url - { - set - { - if(value != null) - content.Action = () => Process.Start(value); - } - } - - public LinkText() - { - AddInternal(content = new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - }); - } - } - - public class ProfileLink : LinkText, IHasTooltip - { - public string TooltipText => "View Profile in Browser"; - - public ProfileLink(User user) - { - Text = user.Username; - Url = $@"https://osu.ppy.sh/users/{user.Id}"; - Font = @"Exo2.0-RegularItalic"; - TextSize = 30; - } + Text = user.Username; + Url = $@"https://osu.ppy.sh/users/{user.Id}"; + Font = @"Exo2.0-RegularItalic"; + TextSize = 30; } } } From 86302716a6047f69d3e793592ae4c000be57e46e Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 20:32:08 +0100 Subject: [PATCH 04/13] Also moved LinkText to its own file so the chat could reuse it (ProfileHeader's private class ProfileLink also still inherits from this, though) --- .../Graphics/Sprites/OsuLinkSpriteText.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs diff --git a/osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs b/osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs new file mode 100644 index 0000000000..111ba6a49a --- /dev/null +++ b/osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Graphics.Containers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.Sprites +{ + public class OsuLinkSpriteText : OsuSpriteText + { + private readonly OsuHoverContainer content; + + public override bool HandleInput => content.Action != null; + + protected override Container Content => content ?? (Container)this; + + protected override IEnumerable FlowingChildren => Children; + + private string url; + + public string Url + { + get + { + return url; + } + set + { + if (value != null) + { + url = value; + content.Action = () => Process.Start(value); + } + } + } + + public OsuLinkSpriteText() + { + AddInternal(content = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + }); + } + } +} From 152eb83c42c4abc3f6608830e8f0514fbaaa0e0d Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 20:33:27 +0100 Subject: [PATCH 05/13] Added new class for chat lines, that colour the messages after formatting. URLs will become blue, and on hover (also defined here) be turned yellow-ish --- osu.Game/Online/Chat/ChatLinkSpriteText.cs | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 osu.Game/Online/Chat/ChatLinkSpriteText.cs diff --git a/osu.Game/Online/Chat/ChatLinkSpriteText.cs b/osu.Game/Online/Chat/ChatLinkSpriteText.cs new file mode 100644 index 0000000000..a47ee7cbb3 --- /dev/null +++ b/osu.Game/Online/Chat/ChatLinkSpriteText.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Online.Chat +{ + public class ChatLinkSpriteText : OsuLinkSpriteText + { + private Color4 hoverColour; + private Color4 urlColour; + + protected override bool OnHover(InputState state) + { + var otherSpritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url && !Equals(child)); + + var hoverResult = base.OnHover(state); + + if (!otherSpritesWithSameLink.Any(sprite => sprite.IsHovered)) + foreach (ChatLinkSpriteText sprite in otherSpritesWithSameLink) + sprite.TriggerOnHover(state); + + Content.FadeColour(hoverColour, 500, Easing.OutQuint); + + return hoverResult; + } + + protected override void OnHoverLost(InputState state) + { + var spritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url); + + if (spritesWithSameLink.Any(sprite => sprite.IsHovered)) + { + // We have to do this so this sprite does not fade its colour back + Content.FadeColour(hoverColour, 500, Easing.OutQuint); + return; + } + + foreach (ChatLinkSpriteText sprite in spritesWithSameLink) + sprite.Content.FadeColour(urlColour, 500, Easing.OutQuint); + + base.OnHoverLost(state); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + hoverColour = colours.Yellow; + urlColour = colours.Blue; + } + } +} From 78ff5d81d3cc9a5f253327f8a61b9bfba108ca29 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 21:03:41 +0100 Subject: [PATCH 06/13] Fixed casting --- osu.Game/Online/Chat/ChatLinkSpriteText.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/ChatLinkSpriteText.cs b/osu.Game/Online/Chat/ChatLinkSpriteText.cs index a47ee7cbb3..a1856af6b0 100644 --- a/osu.Game/Online/Chat/ChatLinkSpriteText.cs +++ b/osu.Game/Online/Chat/ChatLinkSpriteText.cs @@ -23,7 +23,8 @@ namespace osu.Game.Online.Chat protected override bool OnHover(InputState state) { - var otherSpritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url && !Equals(child)); + // Every word is one sprite in chat (for word wrap) so we need to find all other sprites that display the same link + var otherSpritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url && !Equals(child)); var hoverResult = base.OnHover(state); @@ -38,7 +39,7 @@ namespace osu.Game.Online.Chat protected override void OnHoverLost(InputState state) { - var spritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url); + var spritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url); if (spritesWithSameLink.Any(sprite => sprite.IsHovered)) { From 7f1f886406450b146c415c862343f3a54174a9c4 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 21:04:24 +0100 Subject: [PATCH 07/13] implemented method formatting into chat. Also added all necessary files to the .csproj --- osu.Game/Overlays/Chat/ChatLine.cs | 42 +++++++++++++++++++++++------- osu.Game/osu.Game.csproj | 4 +++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 32f933ff42..0e1d383f2a 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -67,12 +67,13 @@ namespace osu.Game.Overlays.Chat private const float text_size = 20; private Color4 customUsernameColour; + private Color4 urlColour; private OsuSpriteText timestamp; public ChatLine(Message message) { - Message = message; + Message = MessageFormatter.FormatMessage(message); RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -82,7 +83,7 @@ namespace osu.Game.Overlays.Chat private Message message; private OsuSpriteText username; - private OsuTextFlowContainer contentFlow; + private OsuLinkTextFlowContainer contentFlow; public Message Message { @@ -104,6 +105,7 @@ namespace osu.Game.Overlays.Chat private void load(OsuColour colours) { customUsernameColour = colours.ChatBlue; + urlColour = colours.Blue; } private bool senderHasBackground => !string.IsNullOrEmpty(message.Sender.Colour); @@ -187,7 +189,7 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Left = message_padding + padding }, Children = new Drawable[] { - contentFlow = new OsuTextFlowContainer(t => { t.TextSize = text_size; }) + contentFlow = new OsuLinkTextFlowContainer(t => { t.TextSize = text_size; }) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -210,16 +212,38 @@ namespace osu.Game.Overlays.Chat timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"; username.Text = $@"{message.Sender.Username}" + (senderHasBackground ? "" : ":"); - if (message.IsAction) + contentFlow.Clear(); + + if (message.Links == null || message.Links.Count == 0) { - contentFlow.Clear(); - contentFlow.AddText("[", sprite => sprite.Padding = new MarginPadding { Right = action_padding }); - contentFlow.AddText(message.Content, sprite => sprite.Font = @"Exo2.0-MediumItalic"); - contentFlow.AddText("]", sprite => sprite.Padding = new MarginPadding { Left = action_padding }); + contentFlow.AddText(message.Content, sprite => + { + if (message.IsAction) + sprite.Font = @"Exo2.0-MediumItalic"; + }); + + return; } else - contentFlow.Text = message.Content; + { + int prevIndex = 0; + foreach (var link in message.Links) + { + contentFlow.AddText(message.Content.Substring(prevIndex, link.Index - prevIndex)); + prevIndex = link.Index + link.Length; + contentFlow.AddLink(message.Content.Substring(link.Index, link.Length), link.Url, sprite => + { + if (message.IsAction) + sprite.Font = @"Exo2.0-MediumItalic"; + sprite.Colour = urlColour; + }); + } + + // Add text after the last link + var lastLink = message.Links[message.Links.Count - 1]; + contentFlow.AddText(message.Content.Substring(lastLink.Index + lastLink.Length)); + } } private class MessageSender : OsuClickableContainer, IHasContextMenu diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f814cbb3d3..665acd2102 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -268,6 +268,8 @@ + + @@ -286,6 +288,8 @@ + + From ade7311c152caed05ec6904ca4c1622fbe402789 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 1 Dec 2017 21:31:12 +0100 Subject: [PATCH 08/13] Updated implementation to be based around a "LinkId" (atm the position of the link, anything unique to a link inside its message will be fine), which does not allow matching (OnHover related) between different links --- osu.Game/Online/Chat/ChatLinkSpriteText.cs | 6 ++++-- osu.Game/Overlays/Chat/ChatLine.cs | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/ChatLinkSpriteText.cs b/osu.Game/Online/Chat/ChatLinkSpriteText.cs index a1856af6b0..560579a007 100644 --- a/osu.Game/Online/Chat/ChatLinkSpriteText.cs +++ b/osu.Game/Online/Chat/ChatLinkSpriteText.cs @@ -18,13 +18,15 @@ namespace osu.Game.Online.Chat { public class ChatLinkSpriteText : OsuLinkSpriteText { + public int LinkId; + private Color4 hoverColour; private Color4 urlColour; protected override bool OnHover(InputState state) { // Every word is one sprite in chat (for word wrap) so we need to find all other sprites that display the same link - var otherSpritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url && !Equals(child)); + var otherSpritesWithSameLink = ((Container)Parent).Children.Where(child => (child as ChatLinkSpriteText)?.LinkId == LinkId && !Equals(child)); var hoverResult = base.OnHover(state); @@ -39,7 +41,7 @@ namespace osu.Game.Online.Chat protected override void OnHoverLost(InputState state) { - var spritesWithSameLink = ((Container)Parent).Children.Where(child => (child as OsuLinkSpriteText)?.Url == Url); + var spritesWithSameLink = ((Container)Parent).Children.Where(child => (child as ChatLinkSpriteText)?.LinkId == LinkId); if (spritesWithSameLink.Any(sprite => sprite.IsHovered)) { diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 0e1d383f2a..3ebf5cf513 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -237,6 +237,8 @@ namespace osu.Game.Overlays.Chat if (message.IsAction) sprite.Font = @"Exo2.0-MediumItalic"; sprite.Colour = urlColour; + // We want to use something that is unique to every formatted link, so I just use the position of the link + ((ChatLinkSpriteText)sprite).LinkId = prevIndex; }); } From d22a9df1400d8416b9ae173b517b82e7c3a8395a Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Sat, 2 Dec 2017 09:47:20 +0100 Subject: [PATCH 09/13] Added new request for getting the beatmapset from only a difficulty map ID --- .../Online/API/Requests/GetBeatmapRequest.cs | 21 +++++++++++++++++++ osu.Game/Overlays/BeatmapSetOverlay.cs | 14 +++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 osu.Game/Online/API/Requests/GetBeatmapRequest.cs diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs new file mode 100644 index 0000000000..7a72402c1e --- /dev/null +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -0,0 +1,21 @@ +using osu.Game.Beatmaps; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Online.API.Requests +{ + public class GetBeatmapRequest : APIRequest + { + private readonly int beatmapId; + + public GetBeatmapRequest(int beatmapId) + { + this.beatmapId = beatmapId; + } + + protected override string Target => $@"beatmaps/{beatmapId}"; + } +} diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 0d658b346e..17fbe907ca 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -139,6 +139,20 @@ namespace osu.Game.Overlays return true; } + public void ShowBeatmap(int beatmapId) + { + var req = new GetBeatmapRequest(beatmapId); + req.Success += res => + { + if (!res.OnlineBeatmapSetID.HasValue) + return; + + ShowBeatmapSet(res.OnlineBeatmapSetID.Value); + }; + + api.Queue(req); + } + public void ShowBeatmapSet(int beatmapSetId) { // todo: display the overlay while we are loading here. we need to support setting BeatmapSet to null for this to work. From 0aced859087138736fa1606ad8c6af53f4dcb8b4 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Sat, 2 Dec 2017 09:47:59 +0100 Subject: [PATCH 10/13] Changed the LinkID to the link's own Index instead of the previous one (just makes more sense imo) --- osu.Game/Overlays/Chat/ChatLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 3ebf5cf513..bf5855c45b 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -238,7 +238,7 @@ namespace osu.Game.Overlays.Chat sprite.Font = @"Exo2.0-MediumItalic"; sprite.Colour = urlColour; // We want to use something that is unique to every formatted link, so I just use the position of the link - ((ChatLinkSpriteText)sprite).LinkId = prevIndex; + ((ChatLinkSpriteText)sprite).LinkId = link.Index; }); } From 6d9dcc66913d1118b5070ce0b973d95e78bbea79 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Sat, 2 Dec 2017 09:48:55 +0100 Subject: [PATCH 11/13] Added all files to the .csproj and also introduced basic action filtering when you set the URL on an OsuLinkSpriteText object --- .../Graphics/Sprites/OsuLinkSpriteText.cs | 45 ++++++++++++++++++- osu.Game/osu.Game.csproj | 1 + 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs b/osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs index 111ba6a49a..f41bca62ec 100644 --- a/osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuLinkSpriteText.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using System; using System.Collections.Generic; using System.Diagnostics; @@ -20,6 +21,8 @@ namespace osu.Game.Graphics.Sprites { private readonly OsuHoverContainer content; + private BeatmapSetOverlay beatmapSetOverlay; + public override bool HandleInput => content.Action != null; protected override Container Content => content ?? (Container)this; @@ -39,7 +42,7 @@ namespace osu.Game.Graphics.Sprites if (value != null) { url = value; - content.Action = () => Process.Start(value); + loadAction(); } } } @@ -51,5 +54,45 @@ namespace osu.Game.Graphics.Sprites AutoSizeAxes = Axes.Both, }); } + + [BackgroundDependencyLoader] + private void load(BeatmapSetOverlay beatmapSetOverlay) + { + this.beatmapSetOverlay = beatmapSetOverlay; + } + + private void loadAction() + { + if (Url == null || String.IsNullOrEmpty(Url)) + return; + + var url = Url; + + if (url.StartsWith("https://")) url = url.Substring(8); + else if (url.StartsWith("http://")) url = url.Substring(7); + else content.Action = () => Process.Start(Url); + + if (url.StartsWith("osu.ppy.sh/")) + { + url = url.Substring(11); + if (url.StartsWith("s") || url.StartsWith("beatmapsets")) + content.Action = () => beatmapSetOverlay.ShowBeatmapSet(getIdFromUrl(url)); + else if (url.StartsWith("b") || url.StartsWith("beatmaps")) + content.Action = () => beatmapSetOverlay.ShowBeatmap(getIdFromUrl(url)); + // else if (url.StartsWith("d")) Maybe later + } + } + + private int getIdFromUrl(string url) + { + var lastSlashIndex = url.LastIndexOf('/'); + if (lastSlashIndex == url.Length) + { + url = url.Substring(url.Length - 1); + lastSlashIndex = url.LastIndexOf('/'); + } + + return int.Parse(url.Substring(lastSlashIndex + 1)); + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index a2d8a4bbe4..24ca01a3ad 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -286,6 +286,7 @@ 20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs + From efe6245e533cf514dbb6c715aa148f3f8f5a0e17 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Sat, 2 Dec 2017 10:42:38 +0100 Subject: [PATCH 12/13] Fixed a bug where Drawable.Width could potentially be set to NaN (0/0) by checking if last variable > 0. --- osu.Game/Overlays/BeatmapSet/PreviewButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/PreviewButton.cs index 52edd1714f..ef248c02d3 100644 --- a/osu.Game/Overlays/BeatmapSet/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/PreviewButton.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.BeatmapSet { base.Update(); - if (Playing.Value && preview != null) + if (Playing.Value && preview != null && preview.Length > 0) { progress.Width = (float)(preview.CurrentTime / preview.Length); } From 7f029a382b5da0b6c10f6d832554686b4a458968 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Sat, 2 Dec 2017 10:43:19 +0100 Subject: [PATCH 13/13] Made the Chat testcase include a beatmapsetoverlay so links can be clicked from in there. Also had to implement private DI to make it work --- osu.Game.Tests/Visual/TestCaseChatDisplay.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/TestCaseChatDisplay.cs b/osu.Game.Tests/Visual/TestCaseChatDisplay.cs index 85ee224a5e..38f5a7cbe0 100644 --- a/osu.Game.Tests/Visual/TestCaseChatDisplay.cs +++ b/osu.Game.Tests/Visual/TestCaseChatDisplay.cs @@ -2,7 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.ComponentModel; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; using osu.Game.Overlays; namespace osu.Game.Tests.Visual @@ -10,12 +12,26 @@ namespace osu.Game.Tests.Visual [Description("Testing chat api and overlay")] internal class TestCaseChatDisplay : OsuTestCase { + private BeatmapSetOverlay beatmapSetOverlay; + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); + public TestCaseChatDisplay() { Add(new ChatOverlay { State = Visibility.Visible }); + + Add(beatmapSetOverlay = new BeatmapSetOverlay()); + } + + [BackgroundDependencyLoader] + private void load() + { + dependencies.Cache(beatmapSetOverlay); } } }