Changed chat link implementation according to review

This commit is contained in:
FreezyLemon 2017-12-31 00:51:47 +01:00
parent d66d741af2
commit e7721d71f3
14 changed files with 272 additions and 281 deletions

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
@ -47,19 +48,32 @@ namespace osu.Game.Tests.Visual
private void clear() => AddStep("clear messages", textContainer.Clear);
private void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false)
private void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false)
{
var newLine = new ChatLine(new DummyMessage(text, isAction));
var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant));
textContainer.Add(newLine);
AddAssert($"msg #{textContainer.Count} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount);
AddAssert($"msg #{textContainer.Count} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic(newLine.ContentFlow));
AddAssert($"msg #{textContainer.Count} shows link(s)", () => isShowingLinks(newLine.ContentFlow));
AddAssert($"msg #{textContainer.Count} shows link(s)", isShowingLinks);
bool isItalic(OsuTextFlowContainer c) => c.Cast<ChatLink>().All(sprite => sprite.Font == @"Exo2.0-MediumItalic");
bool isItalic(ChatFlowContainer c) => c.Cast<ChatLink>().All(sprite => sprite.Font == @"Exo2.0-MediumItalic");
bool isShowingLinks(OsuTextFlowContainer c) => c.Cast<ChatLink>().All(sprite => sprite.HandleInput && !sprite.TextColour.Equals((SRGBColour)Color4.White)
|| !sprite.HandleInput && sprite.TextColour.Equals((SRGBColour)Color4.White));
bool isShowingLinks()
{
SRGBColour textColour = Color4.White;
bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour);
if (isAction && hasBackground)
textColour = OsuColour.FromHex(newLine.Message.Sender.Colour);
return newLine.ContentFlow
.Cast<ChatLink>()
.All(sprite => sprite.HandleInput && !sprite.TextColour.Equals(textColour)
|| !sprite.HandleInput && sprite.TextColour.Equals(textColour)
// if someone with a background uses /me with a link, the usual link colour is overridden
|| isAction && hasBackground && sprite.HandleInput && !sprite.TextColour.Equals((ColourInfo)Color4.White));
}
}
private void testLinksGeneral()
@ -77,6 +91,9 @@ namespace osu.Game.Tests.Visual
addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/home multiple links] https://osu.ppy.sh/home", 3);
// note that there's 0 links here (they get removed if a channel is not found)
addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).");
addMessageWithChecks("I am important!", 0, false, true);
addMessageWithChecks("feels important", 0, true, true);
addMessageWithChecks("likes to post this [https://osu.ppy.sh/home link].", 1, true, true);
}
private void testAddingLinks()
@ -132,37 +149,27 @@ namespace osu.Game.Tests.Visual
private class DummyMessage : Message
{
private static long messageCounter;
internal static readonly User TEST_SENDER_BACKGROUND = new User
{
Username = @"i-am-important",
Id = 42,
Colour = "#250cc9",
};
internal static readonly User TEST_SENDER = new User
{
Username = @"Somebody",
Id = 1,
Country = new Country { FullName = @"Alien" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
JoinDate = DateTimeOffset.Now.AddDays(-1),
LastVisit = DateTimeOffset.Now,
Age = 1,
ProfileOrder = new[] { "me" },
CountryRank = 1,
Statistics = new UserStatistics
{
Rank = 2148,
PP = 4567.89m
},
RankHistory = new User.RankHistoryData
{
Mode = @"osu",
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray(),
}
};
public new DateTimeOffset Timestamp = DateTimeOffset.Now;
public DummyMessage(string text, bool isAction = false)
public DummyMessage(string text, bool isAction = false, bool isImportant = false)
: base(messageCounter++)
{
Content = text;
IsAction = isAction;
Sender = TEST_SENDER;
Sender = isImportant ? TEST_SENDER_BACKGROUND : TEST_SENDER;
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics.Colour;
using osu.Game.Online.Chat;
using System;
namespace osu.Game.Graphics.Containers
{
public class ChatFlowContainer : OsuTextFlowContainer
{
private readonly Action<ChatLink> defaultCreationParameters;
private ColourInfo urlColour;
public ChatFlowContainer(Action<ChatLink> defaultCreationParameters = null)
{
this.defaultCreationParameters = defaultCreationParameters;
}
public override bool HandleInput => true;
public void AddLink(string text, string url, LinkAction linkType, string linkArgument)
{
var chatSprite = new ChatLink
{
Text = text,
Url = url,
TextColour = urlColour,
LinkAction = linkType,
LinkArgument = linkArgument,
};
defaultCreationParameters?.Invoke(chatSprite);
AddInternal(chatSprite);
}
public void AddText(string text, Action<ChatLink> creationParameters = null)
{
foreach (var word in SplitWords(text))
{
if (string.IsNullOrEmpty(word))
continue;
var chatSprite = new ChatLink { Text = word };
defaultCreationParameters?.Invoke(chatSprite);
creationParameters?.Invoke(chatSprite);
AddInternal(chatSprite);
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
urlColour = colours.Blue;
}
}
}

View File

@ -1,59 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using System;
using System.Collections.Generic;
namespace osu.Game.Graphics.Containers
{
public class OsuLinkFlowContainer : OsuLinkFlowContainer<OsuSpriteLink>
{
public OsuLinkFlowContainer(Action<SpriteText> defaultCreationParameters = null)
: base(defaultCreationParameters)
{
}
}
public class OsuLinkFlowContainer<T> : OsuTextFlowContainer
where T : OsuSpriteLink, new()
{
public override bool HandleInput => true;
public OsuLinkFlowContainer(Action<SpriteText> defaultCreationParameters = null)
: base(defaultCreationParameters)
{
}
protected override SpriteText CreateSpriteText() => new T();
/// <summary>
/// The colour for text (links override this). Will only be used for new text elements.
/// </summary>
public ColourInfo TextColour = Color4.White;
public IEnumerable<SpriteText> AddLink(string text, string url, Action<SpriteText> creationParameters = null)
{
// TODO: Remove this and get word wrapping working
text = text.Replace(' ', '_');
return AddText(text, link =>
{
((OsuSpriteLink)link).Url = url;
creationParameters?.Invoke(link);
});
}
public new IEnumerable<SpriteText> AddText(string text, Action<SpriteText> creationParameters = null)
{
return base.AddText(text, sprite =>
{
((OsuSpriteLink)sprite).TextColour = TextColour;
creationParameters?.Invoke(sprite);
});
}
}
}

View File

@ -12,6 +12,8 @@ namespace osu.Game.Graphics.Sprites
{
public class OsuSpriteLink : OsuSpriteText
{
public override bool HandleInput => !string.IsNullOrEmpty(Url);
protected override IEnumerable<Drawable> FlowingChildren => Children;
protected override Container<Drawable> Content => content;

View File

@ -1,19 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapRequest : APIRequest<BeatmapInfo>
{
private readonly int beatmapId;
public GetBeatmapRequest(int beatmapId)
{
this.beatmapId = beatmapId;
}
protected override string Target => $@"beatmaps/{beatmapId}";
}
}

View File

@ -4,123 +4,57 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using System;
using System.Text.RegularExpressions;
namespace osu.Game.Online.Chat
{
public class ChatLink : OsuSpriteLink, IHasTooltip
{
private APIAccess api;
private BeatmapSetOverlay beatmapSetOverlay;
private ChatOverlay chat;
private OsuGame game;
public override bool HandleInput => !string.IsNullOrEmpty(Url);
/// <summary>
/// The type of action executed on clicking this link.
/// </summary>
public LinkAction LinkAction { get; set; }
// 'protocol' -> 'https', 'http', 'osu', 'osump' etc.
// 'content' -> everything after '<protocol>://'
private Match getUrlMatch() => Regex.Match(Url, @"^(?<protocol>osu(?:mp)?|https?):\/\/(?<content>.*)");
/// <summary>
/// The argument necessary for the action specified by <see cref="LinkAction"/> to execute.
/// <para>Usually a part of the URL.</para>
/// </summary>
public string LinkArgument { get; set; }
protected override void OnLinkClicked()
{
var urlMatch = getUrlMatch();
if (urlMatch.Success)
switch (LinkAction)
{
var args = urlMatch.Groups["content"].Value.Split('/');
switch (urlMatch.Groups["protocol"].Value)
{
case "osu":
if (args.Length == 1)
{
base.OnLinkClicked();
break;
}
switch (args[0])
{
case "chan":
var foundChannel = chat.AvailableChannels.Find(channel => channel.Name == args[1]);
// links should be filtered out by now if a channel doesn't exist
chat.OpenChannel(foundChannel ?? throw new ArgumentException($"Unknown channel name ({args[1]})."));
break;
case "edit":
chat.Game?.LoadEditorTimestamp();
break;
case "b":
if (args.Length > 1 && int.TryParse(args[1], out int mapId))
beatmapSetOverlay.ShowBeatmap(mapId);
break;
case "s":
case "dl":
if (args.Length > 1 && int.TryParse(args[1], out int mapSetId))
beatmapSetOverlay.ShowBeatmapSet(mapSetId);
break;
case "spectate":
GetUserRequest req;
if (int.TryParse(args[1], out int userId))
req = new GetUserRequest(userId);
else
return;
req.Success += user =>
{
chat.Game?.LoadSpectatorScreen();
};
api.Queue(req);
break;
default:
throw new ArgumentException($"Unknown osu:// link at {nameof(ChatLink)} ({urlMatch.Groups["content"].Value}).");
}
break;
case "osump":
if (args.Length > 1 && int.TryParse(args[1], out int multiId))
chat.Game?.LoadMultiplayerLobby(multiId);
break;
case "http":
case "https":
if (args[0] == "osu.ppy.sh" && args.Length > 2)
{
switch (args[1])
{
case "b":
case "beatmaps":
beatmapSetOverlay.ShowBeatmap(getId(args[2]));
break;
case "s":
case "beatmapsets":
case "d":
beatmapSetOverlay.ShowBeatmapSet(getId(args[2]));
break;
default:
base.OnLinkClicked();
break;
}
}
else
base.OnLinkClicked();
break;
default:
base.OnLinkClicked();
break;
}
}
else
base.OnLinkClicked();
int getId(string input)
{
var index = input.IndexOf('#');
return int.Parse(index > 0 ? input.Remove(index) : input);
case LinkAction.OpenBeatmap:
// todo: implement this when overlay.ShowBeatmap(id) exists
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(LinkArgument, out int setId))
beatmapSetOverlay.ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
chat.OpenChannel(chat.AvailableChannels.Find(c => c.Name == LinkArgument));
break;
case LinkAction.OpenEditorTimestamp:
game?.LoadEditorTimestamp();
break;
case LinkAction.JoinMultiplayerMatch:
if (int.TryParse(LinkArgument, out int matchId))
game?.JoinMultiplayerMatch(matchId);
break;
case LinkAction.Spectate:
// todo: implement this when spectating exists
break;
case LinkAction.External:
base.OnLinkClicked();
break;
default:
throw new NotImplementedException($"This {nameof(Chat.LinkAction)} ({LinkAction.ToString()}) is missing an associated action.");
}
}
@ -131,30 +65,25 @@ namespace osu.Game.Online.Chat
if (Url == Text)
return null;
var urlMatch = getUrlMatch();
if (urlMatch.Success && urlMatch.Groups["protocol"].Value == "osu")
switch (LinkAction)
{
var args = urlMatch.Groups["content"].Value.Split('/');
if (args.Length < 2)
case LinkAction.OpenChannel:
return "Switch to channel " + LinkArgument;
case LinkAction.OpenEditorTimestamp:
return "Go to " + LinkArgument;
default:
return Url;
if (args[0] == "chan")
return "Switch to channel " + args[1];
if (args[0] == "edit")
return "Go to " + args[1];
}
return Url;
}
}
[BackgroundDependencyLoader]
private void load(APIAccess api, BeatmapSetOverlay beatmapSetOverlay, ChatOverlay chat)
[BackgroundDependencyLoader(true)]
private void load(BeatmapSetOverlay beatmapSetOverlay, ChatOverlay chat, OsuGame game)
{
this.api = api;
this.beatmapSetOverlay = beatmapSetOverlay;
this.chat = chat;
// this will be null in tests
this.game = game;
}
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Online.Chat
{
}
public List<MessageFormatter.Link> Links;
public List<Link> Links;
public Message(long? id)
{

View File

@ -39,7 +39,7 @@ namespace osu.Game.Online.Chat
// Unicode emojis
private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])");
private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0)
private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null)
{
int captureOffset = 0;
foreach (Match m in regex.Matches(result.Text, startIndex))
@ -66,7 +66,8 @@ namespace osu.Game.Online.Chat
//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));
var details = getLinkDetails(link);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.linkType, details.linkArgument));
//adjust the offset for processing the current matches group.
captureOffset += m.Length - displayText.Length;
@ -93,7 +94,70 @@ namespace osu.Game.Online.Chat
}
}
result.Links.Add(new Link(link, index, indexLength));
var details = getLinkDetails(link);
result.Links.Add(new Link(link, index, indexLength, details.linkType, details.linkArgument));
}
}
private static (LinkAction linkType, string linkArgument) getLinkDetails(string url)
{
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])
{
case "http":
case "https":
// length > 3 since all these links need another argument to work
if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh"))
{
switch (args[2])
{
case "b":
case "beatmaps":
return (LinkAction.OpenBeatmap, args[3]);
case "s":
case "beatmapsets":
case "d":
return (LinkAction.External, args[3]);
}
}
return (LinkAction.External, null);
case "osu":
// every internal link also needs some kind of argument
if (args.Length < 3)
return (LinkAction.External, null);
LinkAction linkType;
switch (args[1])
{
case "chan":
linkType = LinkAction.OpenChannel;
break;
case "edit":
linkType = LinkAction.OpenEditorTimestamp;
break;
case "b":
linkType = LinkAction.OpenBeatmap;
break;
case "s":
case "dl":
linkType = LinkAction.OpenBeatmapSet;
break;
case "spectate":
linkType = LinkAction.Spectate;
break;
default:
linkType = LinkAction.External;
break;
}
return (linkType, args[2]);
case "osump":
return (LinkAction.JoinMultiplayerMatch, args[1]);
default:
return (LinkAction.External, null);
}
}
@ -114,10 +178,10 @@ namespace osu.Game.Online.Chat
handleAdvanced(advanced_link_regex, result, startIndex);
// handle editor times
handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex);
handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp);
// handle channels
handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex);
handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex, LinkAction.OpenChannel);
var empty = "";
while (space-- > 0)
@ -151,21 +215,36 @@ namespace osu.Game.Online.Chat
OriginalText = Text = text;
}
}
}
public class Link : IComparable<Link>
public enum LinkAction
{
External,
OpenBeatmap,
OpenBeatmapSet,
OpenChannel,
OpenEditorTimestamp,
JoinMultiplayerMatch,
Spectate,
}
public class Link : IComparable<Link>
{
public string Url;
public int Index;
public int Length;
public LinkAction Action;
public string Argument;
public Link(string url, int startIndex, int length, LinkAction action, string argument)
{
public string Url;
public int Index;
public int Length;
public Link(string url, int startIndex, int length)
{
Url = url;
Index = startIndex;
Length = length;
}
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
Url = url;
Index = startIndex;
Length = length;
Action = action;
Argument = argument;
}
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
}
}

View File

@ -134,7 +134,7 @@ namespace osu.Game
});
}
internal void LoadMultiplayerLobby(int lobbyId)
internal void JoinMultiplayerMatch(int matchId)
{
notifications.Post(new SimpleNotification
{

View File

@ -139,20 +139,6 @@ 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.

View File

@ -83,13 +83,13 @@ namespace osu.Game.Overlays.Chat
private Message message;
private OsuSpriteText username;
private OsuLinkFlowContainer<ChatLink> contentFlow;
private ChatFlowContainer contentFlow;
public OsuTextFlowContainer ContentFlow => contentFlow;
public ChatFlowContainer ContentFlow => contentFlow;
public Message Message
{
get { return message; }
get => message;
set
{
if (message == value) return;
@ -107,7 +107,6 @@ namespace osu.Game.Overlays.Chat
private void load(OsuColour colours, ChatOverlay chat)
{
this.chat = chat;
urlColour = colours.Blue;
customUsernameColour = colours.ChatBlue;
}
@ -192,10 +191,16 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Left = message_padding + padding },
Children = new Drawable[]
{
contentFlow = new OsuLinkFlowContainer<ChatLink>(t =>
contentFlow = new ChatFlowContainer(t =>
{
if (Message.IsAction)
{
t.Font = @"Exo2.0-MediumItalic";
if (senderHasBackground)
t.TextColour = OsuColour.FromHex(message.Sender.Colour);
}
t.TextSize = text_size;
})
{
@ -205,15 +210,12 @@ namespace osu.Game.Overlays.Chat
}
}
};
if (message.IsAction && senderHasBackground)
contentFlow.TextColour = OsuColour.FromHex(message.Sender.Colour);
updateMessageContent();
FinishTransforms(true);
}
private ChatOverlay chat;
private Color4 urlColour;
private void updateMessageContent()
{
@ -230,7 +232,7 @@ namespace osu.Game.Overlays.Chat
else
{
int lastLinkEndIndex = 0;
List<MessageFormatter.Link> linksToRemove = new List<MessageFormatter.Link>();
List<Link> linksToRemove = new List<Link>();
foreach (var link in message.Links)
{
@ -250,10 +252,7 @@ namespace osu.Game.Overlays.Chat
}
}
contentFlow.AddLink(message.Content.Substring(link.Index, link.Length), link.Url, sprite =>
{
((ChatLink)sprite).TextColour = urlColour;
});
contentFlow.AddLink(message.Content.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument);
}
var lastLink = message.Links[message.Links.Count - 1];

View File

@ -41,11 +41,6 @@ namespace osu.Game.Overlays
private readonly FocusedTextBox textbox;
/// <summary>
/// The current OsuGame instance. Will be null for Tests.
/// </summary>
public OsuGame Game;
private APIAccess api;
private const int transition_length = 500;
@ -275,11 +270,9 @@ namespace osu.Game.Overlays
base.PopOut();
}
[BackgroundDependencyLoader(true)]
private void load(APIAccess api, OsuConfigManager config, OsuColour colours, OsuGame game)
[BackgroundDependencyLoader]
private void load(APIAccess api, OsuConfigManager config, OsuColour colours)
{
// game will be null in testing, so some links will not work
Game = game;
this.api = api;
api.Register(this);

View File

@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Profile
public class ProfileHeader : Container
{
private readonly OsuTextFlowContainer infoTextLeft;
private readonly OsuLinkFlowContainer infoTextRight;
private readonly LinkFlowContainer infoTextRight;
private readonly FillFlowContainer<SpriteText> scoreText, scoreNumberText;
private readonly RankGraph rankGraph;
@ -142,7 +142,7 @@ namespace osu.Game.Overlays.Profile
ParagraphSpacing = 0.8f,
LineSpacing = 0.2f
},
infoTextRight = new OsuLinkFlowContainer(t =>
infoTextRight = new LinkFlowContainer(t =>
{
t.TextSize = 14;
t.Font = @"Exo2.0-RegularItalic";
@ -473,6 +473,20 @@ namespace osu.Game.Overlays.Profile
}
}
private class LinkFlowContainer : OsuTextFlowContainer
{
public override bool HandleInput => true;
public LinkFlowContainer(Action<SpriteText> defaultCreationParameters = null)
: base(defaultCreationParameters)
{
}
protected override SpriteText CreateSpriteText() => new OsuSpriteLink();
public void AddLink(string text, string url) => AddText(text, sprite => ((OsuSpriteLink)sprite).Url = url);
}
private class ProfileLink : OsuSpriteLink, IHasTooltip
{
public string TooltipText => "View Profile in Browser";

View File

@ -268,9 +268,10 @@
<Compile Include="Beatmaps\Formats\LegacyStoryboardDecoder.cs" />
<Compile Include="Database\DatabaseContextFactory.cs" />
<Compile Include="Database\IHasPrimaryKey.cs" />
<Compile Include="Graphics\Containers\ChatFlowContainer.cs" />
<Compile Include="Online\API\Requests\GetUserRequest.cs" />
<Compile Include="Overlays\Profile\SupporterIcon.cs" />
<Compile Include="Overlays\Settings\DangerousSettingsButton.cs" />
<Compile Include="Graphics\Containers\OsuLinkFlowContainer.cs" />
<Compile Include="Graphics\Sprites\OsuSpriteLink.cs" />
<Compile Include="Graphics\UserInterface\HoverClickSounds.cs" />
<Compile Include="Graphics\UserInterface\HoverSounds.cs" />
@ -293,7 +294,6 @@
<DependentUpon>20171209034410_AddRulesetInfoShortName.cs</DependentUpon>
</Compile>
<Compile Include="Migrations\OsuDbContextModelSnapshot.cs" />
<Compile Include="Online\API\Requests\GetBeatmapRequest.cs" />
<Compile Include="Online\API\Requests\GetBeatmapSetRequest.cs" />
<Compile Include="Online\Chat\ChatLink.cs" />
<Compile Include="Online\Chat\MessageFormatter.cs" />
@ -441,7 +441,6 @@
<Compile Include="Online\API\Requests\SearchBeatmapSetsRequest.cs" />
<Compile Include="Online\API\Requests\GetMessagesRequest.cs" />
<Compile Include="Online\API\Requests\GetScoresRequest.cs" />
<Compile Include="Online\API\Requests\GetUserRequest.cs" />
<Compile Include="Online\API\Requests\GetUsersRequest.cs" />
<Compile Include="Online\API\Requests\ListChannelsRequest.cs" />
<Compile Include="Online\API\Requests\PostMessageRequest.cs" />