diff --git a/osu.Desktop/Updater/SimpleUpdateManager.cs b/osu.Desktop/Updater/SimpleUpdateManager.cs
index 6c363422f7..e404ccd2b3 100644
--- a/osu.Desktop/Updater/SimpleUpdateManager.cs
+++ b/osu.Desktop/Updater/SimpleUpdateManager.cs
@@ -41,24 +41,32 @@ namespace osu.Desktop.Updater
 
         private async void checkForUpdateAsync()
         {
-            var releases = new JsonWebRequest<GitHubRelease>("https://api.github.com/repos/ppy/osu/releases/latest");
-            await releases.PerformAsync();
-
-            var latest = releases.ResponseObject;
-
-            if (latest.TagName != version)
+            try
             {
-                notificationOverlay.Post(new SimpleNotification
+                var releases = new JsonWebRequest<GitHubRelease>("https://api.github.com/repos/ppy/osu/releases/latest");
+
+                await releases.PerformAsync();
+
+                var latest = releases.ResponseObject;
+
+                if (latest.TagName != version)
                 {
-                    Text = $"A newer release of osu! has been found ({version} → {latest.TagName}).\n\n"
-                           + "Click here to download the new version, which can be installed over the top of your existing installation",
-                    Icon = FontAwesome.fa_upload,
-                    Activated = () =>
+                    notificationOverlay.Post(new SimpleNotification
                     {
-                        host.OpenUrlExternally(getBestUrl(latest));
-                        return true;
-                    }
-                });
+                        Text = $"A newer release of osu! has been found ({version} → {latest.TagName}).\n\n"
+                               + "Click here to download the new version, which can be installed over the top of your existing installation",
+                        Icon = FontAwesome.fa_upload,
+                        Activated = () =>
+                        {
+                            host.OpenUrlExternally(getBestUrl(latest));
+                            return true;
+                        }
+                    });
+                }
+            }
+            catch
+            {
+                // we shouldn't crash on a web failure. or any failure for the matter.
             }
         }
 
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 09bfdc67d4..ab65541ecf 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -28,8 +28,8 @@
   <ItemGroup Label="Package References">
     <PackageReference Include="System.IO.Packaging" Version="4.5.0" />
     <PackageReference Include="ppy.squirrel.windows" Version="1.9.0.3" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.4" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.0" />
   </ItemGroup>
   <ItemGroup Label="Resources">
     <EmbeddedResource Include="lazer.ico" />
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index b76f591239..40f2375251 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -4,7 +4,7 @@
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
+    <PackageReference Include="NUnit3TestAdapter" Version="3.11.2" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 98ad086c66..12bf9759c4 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -4,7 +4,7 @@
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
+    <PackageReference Include="NUnit3TestAdapter" Version="3.11.2" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 6117812f45..3e06aab0e5 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -4,7 +4,7 @@
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
+    <PackageReference Include="NUnit3TestAdapter" Version="3.11.2" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 3ba64398f3..e0097bf9c3 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -4,7 +4,7 @@
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
+    <PackageReference Include="NUnit3TestAdapter" Version="3.11.2" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 6d64b25906..f0211e1ead 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -295,6 +295,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
                 Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[1]).LookupNames.First());
                 Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
                 Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
+
+                // The control point at the end time of the slider should be applied
+                Assert.AreEqual("soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
             }
 
             SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
diff --git a/osu.Game.Tests/Resources/controlpoint-custom-samplebank.osu b/osu.Game.Tests/Resources/controlpoint-custom-samplebank.osu
index 1e0e6f558e..8e7c504109 100644
--- a/osu.Game.Tests/Resources/controlpoint-custom-samplebank.osu
+++ b/osu.Game.Tests/Resources/controlpoint-custom-samplebank.osu
@@ -8,9 +8,12 @@ SampleSet: Normal
 2638,-100,4,1,1,40,0,0
 3107,-100,4,1,2,40,0,0
 3576,-100,4,1,0,40,0,0
+18287,-100,4,2,11,80,0,1
+18595,-100,4,2,8,80,0,1
 
 [HitObjects]
 255,193,2170,1,0,0:0:0:0:
 256,191,2638,5,0,0:0:0:0:
 255,193,3107,1,0,0:0:0:0:
 256,191,3576,1,0,0:0:0:0:
+112,200,18493,6,0,L|104:248,1,35,8|0,0:0|0:0,0:0:0:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
index 66363deb7c..7ee7acd539 100644
--- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
+++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
@@ -100,7 +100,7 @@ namespace osu.Game.Tests.Scores.IO
 
                     var toImport = new ScoreInfo
                     {
-                        Statistics = new Dictionary<HitResult, object>
+                        Statistics = new Dictionary<HitResult, int>
                         {
                             { HitResult.Perfect, 100 },
                             { HitResult.Miss, 50 }
diff --git a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
index 447337bef0..1127402adb 100644
--- a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
+++ b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
@@ -4,15 +4,12 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using osu.Framework.Allocation;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.MathUtils;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
 using osu.Game.Online.Chat;
 using osu.Game.Overlays.Chat.Tabs;
 using osu.Game.Users;
@@ -74,50 +71,50 @@ namespace osu.Game.Tests.Visual
             channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
             channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.ToString();
 
-            AddStep("Add random private channel", addRandomUser);
+            AddStep("Add random private channel", addRandomPrivateChannel);
             AddAssert("There is only one channels", () => channelTabControl.Items.Count() == 2);
-            AddRepeatStep("Add 3 random private channels", addRandomUser, 3);
+            AddRepeatStep("Add 3 random private channels", addRandomPrivateChannel, 3);
             AddAssert("There are four channels", () => channelTabControl.Items.Count() == 5);
             AddStep("Add random public channel", () => addChannel(RNG.Next().ToString()));
 
-            AddRepeatStep("Select a random channel", () => channelTabControl.Current.Value = channelTabControl.Items.ElementAt(RNG.Next(channelTabControl.Items.Count())), 20);
-        }
+            AddRepeatStep("Select a random channel", () => channelTabControl.Current.Value = channelTabControl.Items.ElementAt(RNG.Next(channelTabControl.Items.Count() - 1)), 20);
 
-        private List<User> users;
+            Channel channelBefore = channelTabControl.Items.First();
+            AddStep("set first channel", () => channelTabControl.Current.Value = channelBefore);
 
-        private void addRandomUser()
-        {
-            channelTabControl.AddChannel(new Channel
+            AddStep("select selector tab", () => channelTabControl.Current.Value = channelTabControl.Items.Last());
+            AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
+
+            AddAssert("check channel unchanged", () => channelBefore == channelTabControl.Current.Value);
+
+            AddStep("set second channel", () => channelTabControl.Current.Value = channelTabControl.Items.Skip(1).First());
+            AddAssert("selector tab is inactive", () => !channelTabControl.ChannelSelectorActive.Value);
+
+            AddUntilStep(() =>
             {
-                Users =
-                {
-                    users?.Count > 0
-                        ? users[RNG.Next(0, users.Count - 1)]
-                        : new User
-                        {
-                            Id = RNG.Next(),
-                            Username = "testuser" + RNG.Next(1000)
-                        }
-                }
-            });
+                var first = channelTabControl.Items.First();
+                if (first.Name == "+")
+                    return true;
+
+                channelTabControl.RemoveChannel(first);
+                return false;
+            }, "remove all channels");
+
+            AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
         }
 
-        private void addChannel(string name)
-        {
+        private void addRandomPrivateChannel() =>
+            channelTabControl.AddChannel(new Channel(new User
+            {
+                Id = RNG.Next(1000, 10000000),
+                Username = "Test User " + RNG.Next(1000)
+            }));
+
+        private void addChannel(string name) =>
             channelTabControl.AddChannel(new Channel
             {
                 Type = ChannelType.Public,
                 Name = name
             });
-        }
-
-        [BackgroundDependencyLoader]
-        private void load(IAPIProvider api)
-        {
-            GetUsersRequest req = new GetUsersRequest();
-            req.Success += list => users = list.Select(e => e.User).ToList();
-
-            api.Queue(req);
-        }
     }
 }
diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
index 87235add37..d87a8d0056 100644
--- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
@@ -98,8 +98,11 @@ namespace osu.Game.Tests.Visual
         [SetUp]
         public virtual void SetUp()
         {
-            manager?.Delete(manager.GetAllUsableBeatmapSets());
-            Child = songSelect = new TestSongSelect();
+            Schedule(() =>
+            {
+                manager?.Delete(manager.GetAllUsableBeatmapSets());
+                Child = songSelect = new TestSongSelect();
+            });
         }
 
         [Test]
diff --git a/osu.Game.Tests/Visual/TestCasePollingComponent.cs b/osu.Game.Tests/Visual/TestCasePollingComponent.cs
new file mode 100644
index 0000000000..bf129bfd53
--- /dev/null
+++ b/osu.Game.Tests/Visual/TestCasePollingComponent.cs
@@ -0,0 +1,144 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Logging;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual
+{
+    public class TestCasePollingComponent : OsuTestCase
+    {
+        private Container pollBox;
+        private TestPoller poller;
+
+        private const float safety_adjust = 1f;
+        private int count;
+
+        [SetUp]
+        public void SetUp() => Schedule(() =>
+        {
+            count = 0;
+
+            Children = new Drawable[]
+            {
+                pollBox = new Container
+                {
+                    Alpha = 0,
+                    RelativeSizeAxes = Axes.Both,
+                    Children = new Drawable[]
+                    {
+                        new Box
+                        {
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre,
+                            Scale = new Vector2(0.4f),
+                            Colour = Color4.LimeGreen,
+                            RelativeSizeAxes = Axes.Both,
+                        },
+                        new OsuSpriteText
+                        {
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre,
+                            Text = "Poll!",
+                        }
+                    }
+                }
+            };
+        });
+
+        //[Test]
+        public void TestInstantPolling()
+        {
+            createPoller(true);
+
+            AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust);
+            checkCount(1);
+            checkCount(2);
+            checkCount(3);
+
+            AddStep("set poll interval to 5", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5);
+            checkCount(4);
+            checkCount(4);
+            checkCount(4);
+
+            skip();
+
+            checkCount(5);
+            checkCount(5);
+
+            AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust);
+            checkCount(6);
+            checkCount(7);
+        }
+
+        [Test]
+        public void TestSlowPolling()
+        {
+            createPoller(false);
+
+            AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5);
+            checkCount(0);
+            skip();
+            checkCount(0);
+            skip();
+            skip();
+            skip();
+            skip();
+            checkCount(0);
+            skip();
+            skip();
+            checkCount(0);
+        }
+
+        private void skip() => AddStep("skip", () =>
+        {
+            // could be 4 or 5 at this point due to timing discrepancies (safety_adjust @ 0.2 * 5 ~= 1)
+            // easiest to just ignore the value at this point and move on.
+        });
+
+        private void checkCount(int checkValue)
+        {
+            Logger.Log($"value is {count}");
+            AddAssert($"count is {checkValue}", () => count == checkValue);
+        }
+
+        private void createPoller(bool instant) => AddStep("create poller", () =>
+        {
+            poller?.Expire();
+
+            Add(poller = instant ? new TestPoller() : new TestSlowPoller());
+            poller.OnPoll += () =>
+            {
+                pollBox.FadeOutFromOne(500);
+                count++;
+            };
+        });
+
+        protected override double TimePerAction => 5000;
+
+        public class TestPoller : PollingComponent
+        {
+            public event Action OnPoll;
+
+            protected override Task Poll()
+            {
+                Schedule(() => OnPoll?.Invoke());
+                return base.Poll();
+            }
+        }
+
+        public class TestSlowPoller : TestPoller
+        {
+            protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls / 2f / Clock.Rate)).ContinueWith(_ => base.Poll());
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/TestCaseResults.cs b/osu.Game.Tests/Visual/TestCaseResults.cs
index dfe1cdbfb0..6a20a808b6 100644
--- a/osu.Game.Tests/Visual/TestCaseResults.cs
+++ b/osu.Game.Tests/Visual/TestCaseResults.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual
                 MaxCombo = 123,
                 Rank = ScoreRank.A,
                 Date = DateTimeOffset.Now,
-                Statistics = new Dictionary<HitResult, dynamic>
+                Statistics = new Dictionary<HitResult, int>
                 {
                     { HitResult.Great, 50 },
                     { HitResult.Good, 20 },
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index c0f0695ff8..d45f1dd962 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -2,10 +2,10 @@
   <Import Project="..\osu.TestProject.props" />
   <ItemGroup Label="Package References">
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
-    <PackageReference Include="DeepEqual" Version="1.6.0" />
+    <PackageReference Include="DeepEqual" Version="2.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
-    <PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
+    <PackageReference Include="NUnit3TestAdapter" Version="3.11.2" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 8728d776d0..c179821a7c 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -249,10 +249,13 @@ namespace osu.Game.Beatmaps
         /// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
         /// </summary>
         /// <param name="beatmapInfo">The beatmap to lookup.</param>
-        /// <param name="previous">The currently loaded <see cref="WorkingBeatmap"/>. Allows for optimisation where elements are shared with the new beatmap.</param>
+        /// <param name="previous">The currently loaded <see cref="WorkingBeatmap"/>. Allows for optimisation where elements are shared with the new beatmap. May be returned if beatmapInfo requested matches</param>
         /// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
         public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null)
         {
+            if (beatmapInfo?.ID > 0 && previous != null && previous.BeatmapInfo?.ID == beatmapInfo.ID)
+                return previous;
+
             if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
                 return DefaultBeatmap;
 
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
index 838c4f95e4..b26bc751b9 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Online.API.Requests.Responses
         }
 
         [JsonProperty(@"statistics")]
-        private Dictionary<string, object> jsonStats
+        private Dictionary<string, int> jsonStats
         {
             set
             {
diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs
index 9d3b7b5cc9..9dc357c403 100644
--- a/osu.Game/Online/Chat/Channel.cs
+++ b/osu.Game/Online/Chat/Channel.cs
@@ -88,6 +88,17 @@ namespace osu.Game.Online.Chat
         {
         }
 
+        /// <summary>
+        /// Create a private messaging channel with the specified user.
+        /// </summary>
+        /// <param name="user">The user to create the private conversation with.</param>
+        public Channel(User user)
+        {
+            Type = ChannelType.PM;
+            Users.Add(user);
+            Name = user.Username;
+        }
+
         /// <summary>
         /// Adds the argument message as a local echo. When this local echo is resolved <see cref="PendingMessageResolved"/> will get called.
         /// </summary>
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 29f971078b..a63af0f7a3 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -4,11 +4,10 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading.Tasks;
 using osu.Framework.Allocation;
 using osu.Framework.Configuration;
-using osu.Framework.Graphics;
 using osu.Framework.Logging;
-using osu.Framework.Threading;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
 using osu.Game.Users;
@@ -18,7 +17,7 @@ namespace osu.Game.Online.Chat
     /// <summary>
     /// Manages everything channel related
     /// </summary>
-    public class ChannelManager : Component, IOnlineComponent
+    public class ChannelManager : PollingComponent
     {
         /// <summary>
         /// The channels the player joins on startup
@@ -49,11 +48,14 @@ namespace osu.Game.Online.Chat
         public IBindableCollection<Channel> AvailableChannels => availableChannels;
 
         private IAPIProvider api;
-        private ScheduledDelegate fetchMessagesScheduleder;
+
+        public readonly BindableBool HighPollRate = new BindableBool();
 
         public ChannelManager()
         {
             CurrentChannel.ValueChanged += currentChannelChanged;
+
+            HighPollRate.BindValueChanged(high => TimeBetweenPolls = high ? 1000 : 6000, true);
         }
 
         /// <summary>
@@ -79,7 +81,7 @@ namespace osu.Game.Online.Chat
                 throw new ArgumentNullException(nameof(user));
 
             CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id))
-                                   ?? new Channel { Name = user.Username, Users = { user }, Type = ChannelType.PM };
+                                   ?? new Channel(user);
         }
 
         private void currentChannelChanged(Channel channel) => JoinChannel(channel);
@@ -360,73 +362,60 @@ namespace osu.Game.Online.Chat
             }
         }
 
-        public void APIStateChanged(APIAccess api, APIState state)
-        {
-            switch (state)
-            {
-                case APIState.Online:
-                    fetchUpdates();
-                    break;
-                default:
-                    fetchMessagesScheduleder?.Cancel();
-                    fetchMessagesScheduleder = null;
-                    break;
-            }
-        }
-
         private long lastMessageId;
-        private const int update_poll_interval = 1000;
 
         private bool channelsInitialised;
 
-        private void fetchUpdates()
+        protected override Task Poll()
         {
-            fetchMessagesScheduleder?.Cancel();
-            fetchMessagesScheduleder = Scheduler.AddDelayed(() =>
+            if (!api.IsLoggedIn)
+                return base.Poll();
+
+            var fetchReq = new GetUpdatesRequest(lastMessageId);
+
+            var tcs = new TaskCompletionSource<bool>();
+
+            fetchReq.Success += updates =>
             {
-                var fetchReq = new GetUpdatesRequest(lastMessageId);
-
-                fetchReq.Success += updates =>
+                if (updates?.Presence != null)
                 {
-                    if (updates?.Presence != null)
+                    foreach (var channel in updates.Presence)
                     {
-                        foreach (var channel in updates.Presence)
-                        {
-                            // we received this from the server so should mark the channel already joined.
-                            JoinChannel(channel, true);
-                        }
-
-                        //todo: handle left channels
-
-                        handleChannelMessages(updates.Messages);
-
-                        foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
-                            JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
-
-                        lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
+                        // we received this from the server so should mark the channel already joined.
+                        JoinChannel(channel, true);
                     }
 
-                    if (!channelsInitialised)
-                    {
-                        channelsInitialised = true;
-                        // we want this to run after the first presence so we can see if the user is in any channels already.
-                        initializeChannels();
-                    }
+                    //todo: handle left channels
 
-                    fetchUpdates();
-                };
+                    handleChannelMessages(updates.Messages);
 
-                fetchReq.Failure += delegate { fetchUpdates(); };
+                    foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
+                        JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
 
-                api.Queue(fetchReq);
-            }, update_poll_interval);
+                    lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
+                }
+
+                if (!channelsInitialised)
+                {
+                    channelsInitialised = true;
+                    // we want this to run after the first presence so we can see if the user is in any channels already.
+                    initializeChannels();
+                }
+
+                tcs.SetResult(true);
+            };
+
+            fetchReq.Failure += _ => tcs.SetResult(false);
+
+            api.Queue(fetchReq);
+
+            return tcs.Task;
         }
 
         [BackgroundDependencyLoader]
         private void load(IAPIProvider api)
         {
             this.api = api;
-            api.Register(this);
         }
     }
 
diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs
index de017baf35..2b0a49cb6c 100644
--- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs
+++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.UserInterface;
@@ -15,20 +14,20 @@ using osuTK;
 namespace osu.Game.Online.Chat
 {
     /// <summary>
-    /// An invisible drawable that brings multiple <see cref="SpriteText"/> pieces together to form a consumable clickable link.
+    /// An invisible drawable that brings multiple <see cref="Drawable"/> pieces together to form a consumable clickable link.
     /// </summary>
     public class DrawableLinkCompiler : OsuHoverContainer, IHasTooltip
     {
         /// <summary>
         /// Each word part of a chat link (split for word-wrap support).
         /// </summary>
-        public List<SpriteText> Parts;
+        public List<Drawable> Parts;
 
         public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
 
         protected override HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
 
-        public DrawableLinkCompiler(IEnumerable<SpriteText> parts)
+        public DrawableLinkCompiler(IEnumerable<Drawable> parts)
         {
             Parts = parts.ToList();
         }
@@ -45,9 +44,9 @@ namespace osu.Game.Online.Chat
 
         private class LinkHoverSounds : HoverClickSounds
         {
-            private readonly List<SpriteText> parts;
+            private readonly List<Drawable> parts;
 
-            public LinkHoverSounds(HoverSampleSet sampleSet, List<SpriteText> parts)
+            public LinkHoverSounds(HoverSampleSet sampleSet, List<Drawable> parts)
                 : base(sampleSet)
             {
                 this.parts = parts;
diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs
new file mode 100644
index 0000000000..d9dcfc40c2
--- /dev/null
+++ b/osu.Game/Online/PollingComponent.cs
@@ -0,0 +1,108 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using System.Threading.Tasks;
+using osu.Framework.Graphics;
+using osu.Framework.Threading;
+
+namespace osu.Game.Online
+{
+    /// <summary>
+    /// A component which requires a constant polling process.
+    /// </summary>
+    public abstract class PollingComponent : Component
+    {
+        private double? lastTimePolled;
+
+        private ScheduledDelegate scheduledPoll;
+
+        private bool pollingActive;
+
+        private double timeBetweenPolls;
+
+        /// <summary>
+        /// The time that should be waited between polls.
+        /// </summary>
+        public double TimeBetweenPolls
+        {
+            get => timeBetweenPolls;
+            set
+            {
+                timeBetweenPolls = value;
+                scheduledPoll?.Cancel();
+                pollIfNecessary();
+            }
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+            pollIfNecessary();
+        }
+
+        private bool pollIfNecessary()
+        {
+            // we must be loaded so we have access to clock.
+            if (!IsLoaded) return false;
+
+            // there's already a poll process running.
+            if (pollingActive) return false;
+
+            // don't try polling if the time between polls hasn't been set.
+            if (timeBetweenPolls == 0) return false;
+
+            if (!lastTimePolled.HasValue)
+            {
+                doPoll();
+                return true;
+            }
+
+            if (Time.Current - lastTimePolled.Value > timeBetweenPolls)
+            {
+                doPoll();
+                return true;
+            }
+
+            // not ennough time has passed since the last poll. we do want to schedule a poll to happen, though.
+            scheduleNextPoll();
+            return false;
+        }
+
+        private void doPoll()
+        {
+            scheduledPoll = null;
+            pollingActive = true;
+            Poll().ContinueWith(_ => pollComplete());
+        }
+
+        /// <summary>
+        /// Perform the polling in this method. Call <see cref="pollComplete"/> when done.
+        /// </summary>
+        protected virtual Task Poll()
+        {
+            return Task.CompletedTask;
+        }
+
+        /// <summary>
+        /// Call when a poll operation has completed.
+        /// </summary>
+        private void pollComplete()
+        {
+            lastTimePolled = Time.Current;
+            pollingActive = false;
+
+            if (scheduledPoll == null)
+                scheduleNextPoll();
+        }
+
+        private void scheduleNextPoll()
+        {
+            scheduledPoll?.Cancel();
+
+            double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0;
+
+            scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration));
+        }
+    }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 73ecbafb9e..31a00e68ac 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -418,6 +418,8 @@ namespace osu.Game
             dependencies.Cache(notifications);
             dependencies.Cache(dialogOverlay);
 
+            chatOverlay.StateChanged += state => channelManager.HighPollRate.Value = state == Visibility.Visible;
+
             Add(externalLinkOpener = new ExternalLinkOpener());
 
             var singleDisplaySideOverlays = new OverlayContainer[] { settings, notifications };
diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs
deleted file mode 100644
index 1f8c5d38b9..0000000000
--- a/osu.Game/Overlays/Chat/ChatTabControl.cs
+++ /dev/null
@@ -1,348 +0,0 @@
-// Copyright (c) 2007-2018 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.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.UserInterface;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Chat;
-using osuTK;
-using osuTK.Input;
-using osuTK.Graphics;
-using osu.Framework.Configuration;
-using System;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics.Containers;
-
-namespace osu.Game.Overlays.Chat
-{
-    public class ChatTabControl : OsuTabControl<Channel>
-    {
-        private const float shear_width = 10;
-
-        public Action<Channel> OnRequestLeave;
-
-        public readonly Bindable<bool> ChannelSelectorActive = new Bindable<bool>();
-
-        private readonly ChannelTabItem.ChannelSelectorTabItem selectorTab;
-
-        public ChatTabControl()
-        {
-            TabContainer.Margin = new MarginPadding { Left = 50 };
-            TabContainer.Spacing = new Vector2(-shear_width, 0);
-            TabContainer.Masking = false;
-
-            AddInternal(new SpriteIcon
-            {
-                Icon = FontAwesome.fa_comments,
-                Anchor = Anchor.CentreLeft,
-                Origin = Anchor.CentreLeft,
-                Size = new Vector2(20),
-                Margin = new MarginPadding(10),
-            });
-
-            AddTabItem(selectorTab = new ChannelTabItem.ChannelSelectorTabItem(new Channel { Name = "+" }));
-
-            ChannelSelectorActive.BindTo(selectorTab.Active);
-        }
-
-        protected override void AddTabItem(TabItem<Channel> item, bool addToDropdown = true)
-        {
-            if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue)
-                // performTabSort might've made selectorTab's position wonky, fix it
-                TabContainer.SetLayoutPosition(selectorTab, float.MaxValue);
-
-            base.AddTabItem(item, addToDropdown);
-
-            if (SelectedTab == null)
-                SelectTab(item);
-        }
-
-        protected override TabItem<Channel> CreateTabItem(Channel value) => new ChannelTabItem(value) { OnRequestClose = tabCloseRequested };
-
-        protected override void SelectTab(TabItem<Channel> tab)
-        {
-            if (tab is ChannelTabItem.ChannelSelectorTabItem)
-            {
-                tab.Active.Toggle();
-                return;
-            }
-
-            selectorTab.Active.Value = false;
-
-            base.SelectTab(tab);
-        }
-
-        private void tabCloseRequested(TabItem<Channel> tab)
-        {
-            int totalTabs = TabContainer.Count - 1; // account for selectorTab
-            int currentIndex = MathHelper.Clamp(TabContainer.IndexOf(tab), 1, totalTabs);
-
-            if (tab == SelectedTab && totalTabs > 1)
-                // Select the tab after tab-to-be-removed's index, or the tab before if current == last
-                SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]);
-            else if (totalTabs == 1 && !selectorTab.Active)
-                // Open channel selection overlay if all channel tabs will be closed after removing this tab
-                SelectTab(selectorTab);
-
-            OnRequestLeave?.Invoke(tab.Value);
-        }
-
-        private class ChannelTabItem : TabItem<Channel>
-        {
-            private Color4 backgroundInactive;
-            private Color4 backgroundHover;
-            private Color4 backgroundActive;
-
-            public override bool IsRemovable => !Pinned;
-
-            private readonly SpriteText text;
-            private readonly SpriteText textBold;
-            private readonly ClickableContainer closeButton;
-            private readonly Box box;
-            private readonly Box highlightBox;
-            private readonly SpriteIcon icon;
-
-            public Action<ChannelTabItem> OnRequestClose;
-
-            private void updateState()
-            {
-                if (Active)
-                    fadeActive();
-                else
-                    fadeInactive();
-            }
-
-            private const float transition_length = 400;
-
-            private void fadeActive()
-            {
-                this.ResizeTo(new Vector2(Width, 1.1f), transition_length, Easing.OutQuint);
-
-                box.FadeColour(backgroundActive, transition_length, Easing.OutQuint);
-                highlightBox.FadeIn(transition_length, Easing.OutQuint);
-
-                text.FadeOut(transition_length, Easing.OutQuint);
-                textBold.FadeIn(transition_length, Easing.OutQuint);
-            }
-
-            private void fadeInactive()
-            {
-                this.ResizeTo(new Vector2(Width, 1), transition_length, Easing.OutQuint);
-
-                box.FadeColour(backgroundInactive, transition_length, Easing.OutQuint);
-                highlightBox.FadeOut(transition_length, Easing.OutQuint);
-
-                text.FadeIn(transition_length, Easing.OutQuint);
-                textBold.FadeOut(transition_length, Easing.OutQuint);
-            }
-
-            protected override bool OnMouseUp(MouseUpEvent e)
-            {
-                if (e.Button == MouseButton.Middle)
-                {
-                    closeButton.Action();
-                    return true;
-                }
-
-                return false;
-            }
-
-            protected override bool OnHover(HoverEvent e)
-            {
-                if (IsRemovable)
-                    closeButton.FadeIn(200, Easing.OutQuint);
-
-                if (!Active)
-                    box.FadeColour(backgroundHover, transition_length, Easing.OutQuint);
-                return true;
-            }
-
-            protected override void OnHoverLost(HoverLostEvent e)
-            {
-                closeButton.FadeOut(200, Easing.OutQuint);
-                updateState();
-            }
-
-            [BackgroundDependencyLoader]
-            private void load(OsuColour colours)
-            {
-                backgroundActive = colours.ChatBlue;
-                backgroundInactive = colours.Gray4;
-                backgroundHover = colours.Gray7;
-
-                highlightBox.Colour = colours.Yellow;
-            }
-
-            protected override void LoadComplete()
-            {
-                base.LoadComplete();
-
-                updateState();
-            }
-
-            public ChannelTabItem(Channel value) : base(value)
-            {
-                Width = 150;
-
-                RelativeSizeAxes = Axes.Y;
-
-                Anchor = Anchor.BottomLeft;
-                Origin = Anchor.BottomLeft;
-
-                Shear = new Vector2(shear_width / ChatOverlay.TAB_AREA_HEIGHT, 0);
-
-                Masking = true;
-                EdgeEffect = new EdgeEffectParameters
-                {
-                    Type = EdgeEffectType.Shadow,
-                    Radius = 10,
-                    Colour = Color4.Black.Opacity(0.2f),
-                };
-
-                Children = new Drawable[]
-                {
-                    box = new Box
-                    {
-                        EdgeSmoothness = new Vector2(1, 0),
-                        RelativeSizeAxes = Axes.Both,
-                    },
-                    highlightBox = new Box
-                    {
-                        Width = 5,
-                        Alpha = 0,
-                        Anchor = Anchor.BottomRight,
-                        Origin = Anchor.BottomRight,
-                        EdgeSmoothness = new Vector2(1, 0),
-                        RelativeSizeAxes = Axes.Y,
-                    },
-                    new Container
-                    {
-                        Shear = new Vector2(-shear_width / ChatOverlay.TAB_AREA_HEIGHT, 0),
-                        RelativeSizeAxes = Axes.Both,
-                        Children = new Drawable[]
-                        {
-                            icon = new SpriteIcon
-                            {
-                                Icon = FontAwesome.fa_hashtag,
-                                Anchor = Anchor.CentreLeft,
-                                Origin = Anchor.CentreLeft,
-                                Colour = Color4.Black,
-                                X = -10,
-                                Alpha = 0.2f,
-                                Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT),
-                            },
-                            text = new OsuSpriteText
-                            {
-                                Margin = new MarginPadding(5),
-                                Origin = Anchor.CentreLeft,
-                                Anchor = Anchor.CentreLeft,
-                                Text = value.ToString(),
-                                TextSize = 18,
-                            },
-                            textBold = new OsuSpriteText
-                            {
-                                Alpha = 0,
-                                Margin = new MarginPadding(5),
-                                Origin = Anchor.CentreLeft,
-                                Anchor = Anchor.CentreLeft,
-                                Text = value.ToString(),
-                                Font = @"Exo2.0-Bold",
-                                TextSize = 18,
-                            },
-                            closeButton = new CloseButton
-                            {
-                                Alpha = 0,
-                                Margin = new MarginPadding { Right = 20 },
-                                Origin = Anchor.CentreRight,
-                                Anchor = Anchor.CentreRight,
-                                Action = delegate
-                                {
-                                    if (IsRemovable) OnRequestClose?.Invoke(this);
-                                },
-                            },
-                        },
-                    },
-                };
-            }
-
-            public class CloseButton : OsuClickableContainer
-            {
-                private readonly SpriteIcon icon;
-
-                public CloseButton()
-                {
-                    Size = new Vector2(20);
-
-                    Child = icon = new SpriteIcon
-                    {
-                        Anchor = Anchor.Centre,
-                        Origin = Anchor.Centre,
-                        Scale = new Vector2(0.75f),
-                        Icon = FontAwesome.fa_close,
-                        RelativeSizeAxes = Axes.Both,
-                    };
-                }
-
-                protected override bool OnMouseDown(MouseDownEvent e)
-                {
-                    icon.ScaleTo(0.5f, 1000, Easing.OutQuint);
-                    return base.OnMouseDown(e);
-                }
-
-                protected override bool OnMouseUp(MouseUpEvent e)
-                {
-                    icon.ScaleTo(0.75f, 1000, Easing.OutElastic);
-                    return base.OnMouseUp(e);
-                }
-
-                protected override bool OnHover(HoverEvent e)
-                {
-                    icon.FadeColour(Color4.Red, 200, Easing.OutQuint);
-                    return base.OnHover(e);
-                }
-
-                protected override void OnHoverLost(HoverLostEvent e)
-                {
-                    icon.FadeColour(Color4.White, 200, Easing.OutQuint);
-                    base.OnHoverLost(e);
-                }
-            }
-
-            public class ChannelSelectorTabItem : ChannelTabItem
-            {
-                public override bool IsRemovable => false;
-
-                public override bool IsSwitchable => false;
-
-                public ChannelSelectorTabItem(Channel value) : base(value)
-                {
-                    Depth = float.MaxValue;
-                    Width = 45;
-
-                    icon.Alpha = 0;
-
-                    text.TextSize = 45;
-                    textBold.TextSize = 45;
-                }
-
-                [BackgroundDependencyLoader]
-                private new void load(OsuColour colour)
-                {
-                    backgroundInactive = colour.Gray2;
-                    backgroundActive = colour.Gray3;
-                }
-            }
-
-            protected override void OnActivated() => updateState();
-
-            protected override void OnDeactivated() => updateState();
-        }
-    }
-}
diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs
index ce5d961282..2418eda2b6 100644
--- a/osu.Game/Overlays/Chat/DrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/DrawableChannel.cs
@@ -48,10 +48,6 @@ namespace osu.Game.Overlays.Chat
                     },
                 }
             };
-
-            Channel.NewMessagesArrived += newMessagesArrived;
-            Channel.MessageRemoved += messageRemoved;
-            Channel.PendingMessageResolved += pendingMessageResolved;
         }
 
         protected override void LoadComplete()
@@ -59,6 +55,11 @@ namespace osu.Game.Overlays.Chat
             base.LoadComplete();
 
             newMessagesArrived(Channel.Messages);
+
+            Channel.NewMessagesArrived += newMessagesArrived;
+            Channel.MessageRemoved += messageRemoved;
+            Channel.PendingMessageResolved += pendingMessageResolved;
+
             scrollToEnd();
         }
 
@@ -78,8 +79,6 @@ namespace osu.Game.Overlays.Chat
 
             flow.AddRange(displayMessages.Select(m => new ChatLine(m)));
 
-            if (!IsLoaded) return;
-
             if (scroll.IsScrolledToEnd(10) || !flow.Children.Any() || newMessages.Any(m => m is LocalMessage))
                 scrollToEnd();
 
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
index 0b1721741a..b370d8f3c5 100644
--- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
+++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
@@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Chat.Tabs
     {
         public override bool IsRemovable => false;
 
+        public override bool IsSwitchable => false;
+
         public ChannelSelectorTabItem(Channel value) : base(value)
         {
             Depth = float.MaxValue;
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
index 79cb0a4d14..dc72c8053a 100644
--- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
+++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
@@ -94,13 +94,12 @@ namespace osu.Game.Overlays.Chat.Tabs
         {
             if (tab is ChannelSelectorTabItem)
             {
-                tab.Active.Toggle();
+                tab.Active.Value = true;
                 return;
             }
 
-            selectorTab.Active.Value = false;
-
             base.SelectTab(tab);
+            selectorTab.Active.Value = false;
         }
 
         private void tabCloseRequested(TabItem<Channel> tab)
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index bdb28d1246..75e22f85d1 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -52,9 +52,9 @@ namespace osu.Game.Overlays
         public Bindable<double> ChatHeight { get; set; }
 
         private readonly Container channelSelectionContainer;
-        private readonly ChannelSelectionOverlay channelSelection;
+        private readonly ChannelSelectionOverlay channelSelectionOverlay;
 
-        public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || channelSelection.State == Visibility.Visible && channelSelection.ReceivePositionalInputAt(screenSpacePos);
+        public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || channelSelectionOverlay.State == Visibility.Visible && channelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos);
 
         public ChatOverlay()
         {
@@ -74,7 +74,7 @@ namespace osu.Game.Overlays
                     Masking = true,
                     Children = new[]
                     {
-                        channelSelection = new ChannelSelectionOverlay
+                        channelSelectionOverlay = new ChannelSelectionOverlay
                         {
                             RelativeSizeAxes = Axes.Both,
                         },
@@ -161,9 +161,16 @@ namespace osu.Game.Overlays
             };
 
             channelTabControl.Current.ValueChanged += chat => channelManager.CurrentChannel.Value = chat;
-            channelTabControl.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden;
-            channelSelection.StateChanged += state =>
+            channelTabControl.ChannelSelectorActive.ValueChanged += value => channelSelectionOverlay.State = value ? Visibility.Visible : Visibility.Hidden;
+            channelSelectionOverlay.StateChanged += state =>
             {
+                if (state == Visibility.Hidden && channelManager.CurrentChannel.Value == null)
+                {
+                    channelSelectionOverlay.State = Visibility.Visible;
+                    State = Visibility.Hidden;
+                    return;
+                }
+
                 channelTabControl.ChannelSelectorActive.Value = state == Visibility.Visible;
 
                 if (state == Visibility.Visible)
@@ -176,8 +183,8 @@ namespace osu.Game.Overlays
                     textbox.HoldFocus = true;
             };
 
-            channelSelection.OnRequestJoin = channel => channelManager.JoinChannel(channel);
-            channelSelection.OnRequestLeave = channel => channelManager.LeaveChannel(channel);
+            channelSelectionOverlay.OnRequestJoin = channel => channelManager.JoinChannel(channel);
+            channelSelectionOverlay.OnRequestLeave = channel => channelManager.LeaveChannel(channel);
         }
 
         private void currentChannelChanged(Channel channel)
@@ -186,6 +193,7 @@ namespace osu.Game.Overlays
             {
                 textbox.Current.Disabled = true;
                 currentChannelContainer.Clear(false);
+                channelSelectionOverlay.State = Visibility.Visible;
                 return;
             }
 
@@ -239,7 +247,7 @@ namespace osu.Game.Overlays
                 double targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
 
                 // If the channel selection screen is shown, mind its minimum height
-                if (channelSelection.State == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height)
+                if (channelSelectionOverlay.State == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height)
                     targetChatHeight = 1f - channel_selection_min_height;
 
                 ChatHeight.Value = targetChatHeight;
@@ -305,7 +313,7 @@ namespace osu.Game.Overlays
             channelManager.AvailableChannels.ItemsRemoved += availableChannelsChanged;
 
             //for the case that channelmanager was faster at fetching the channels than our attachment to CollectionChanged.
-            channelSelection.UpdateAvailableChannels(channelManager.AvailableChannels);
+            channelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels);
             foreach (Channel channel in channelManager.JoinedChannels)
                 channelTabControl.AddChannel(channel);
         }
@@ -326,7 +334,7 @@ namespace osu.Game.Overlays
         }
 
         private void availableChannelsChanged(IEnumerable<Channel> channels)
-            => channelSelection.UpdateAvailableChannels(channelManager.AvailableChannels);
+            => channelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels);
 
         protected override void Dispose(bool isDisposing)
         {
diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs
index 2f5bcd129a..b4828e41f6 100644
--- a/osu.Game/Overlays/Music/PlaylistItem.cs
+++ b/osu.Game/Overlays/Music/PlaylistItem.cs
@@ -8,7 +8,6 @@ using osuTK.Graphics;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Events;
 using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
@@ -27,7 +26,7 @@ namespace osu.Game.Overlays.Music
 
         private SpriteIcon handle;
         private TextFlowContainer text;
-        private IEnumerable<SpriteText> titleSprites;
+        private IEnumerable<Drawable> titleSprites;
         private ILocalisedBindableString titleBind;
         private ILocalisedBindableString artistBind;
 
@@ -59,7 +58,7 @@ namespace osu.Game.Overlays.Music
                 selected = value;
 
                 FinishTransforms(true);
-                foreach (SpriteText s in titleSprites)
+                foreach (Drawable s in titleSprites)
                     s.FadeColour(Selected ? hoverColour : Color4.White, fade_duration);
             }
         }
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index f92990fc5d..1df07070a1 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -7,8 +7,8 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Online.API.Requests;
 using osu.Game.Users;
 using System;
+using System.Collections.Generic;
 using System.Linq;
-using osu.Game.Online.API.Requests.Responses;
 
 namespace osu.Game.Overlays.Profile.Sections.Ranks
 {
@@ -39,33 +39,34 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
                 foreach (var s in scores)
                     s.Ruleset = Rulesets.GetRuleset(s.RulesetID);
 
-                ShowMoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0);
-                ShowMoreLoading.Hide();
-
                 if (!scores.Any() && VisiblePages == 1)
                 {
+                    ShowMoreButton.Hide();
+                    ShowMoreLoading.Hide();
                     MissingText.Show();
                     return;
                 }
 
-                MissingText.Hide();
+                IEnumerable<DrawableProfileScore> drawableScores;
 
-                foreach (APIScoreInfo score in scores)
+                switch (type)
                 {
-                    DrawableProfileScore drawableScore;
-
-                    switch (type)
-                    {
-                        default:
-                            drawableScore = new DrawablePerformanceScore(score, includeWeight ? Math.Pow(0.95, ItemsContainer.Count) : (double?)null);
-                            break;
-                        case ScoreType.Recent:
-                            drawableScore = new DrawableTotalScore(score);
-                            break;
-                    }
-
-                    ItemsContainer.Add(drawableScore);
+                    default:
+                        drawableScores = scores.Select(score => new DrawablePerformanceScore(score, includeWeight ? Math.Pow(0.95, ItemsContainer.Count) : (double?)null));
+                        break;
+                    case ScoreType.Recent:
+                        drawableScores = scores.Select(score => new DrawableTotalScore(score));
+                        break;
                 }
+
+                LoadComponentsAsync(drawableScores, s =>
+                {
+                    MissingText.Hide();
+                    ShowMoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0);
+                    ShowMoreLoading.Hide();
+
+                    ItemsContainer.AddRange(s);
+                });
             });
 
             Api.Queue(request);
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 8718269eed..e0728826df 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -147,6 +147,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
 
         /// <summary>
         /// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
+        /// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
         /// </summary>
         public void PlaySamples() => Samples?.Play();
 
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 67a3db7a00..010fc450e0 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -19,6 +19,11 @@ namespace osu.Game.Rulesets.Objects
     /// </summary>
     public class HitObject
     {
+        /// <summary>
+        /// A small adjustment to the start time of control points to account for rounding/precision errors.
+        /// </summary>
+        private const double control_point_leniency = 1;
+
         /// <summary>
         /// The time at which the HitObject starts.
         /// </summary>
@@ -69,6 +74,9 @@ namespace osu.Game.Rulesets.Objects
         {
             ApplyDefaultsToSelf(controlPointInfo, difficulty);
 
+            // This is done here since ApplyDefaultsToSelf may be used to determine the end time
+            SampleControlPoint = controlPointInfo.SamplePointAt(((this as IHasEndTime)?.EndTime ?? StartTime) + control_point_leniency);
+
             nestedHitObjects.Clear();
 
             CreateNestedHitObjects();
@@ -84,11 +92,7 @@ namespace osu.Game.Rulesets.Objects
 
         protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
         {
-            SampleControlPoint samplePoint = controlPointInfo.SamplePointAt(StartTime);
-            EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime);
-
-            Kiai = effectPoint.KiaiMode;
-            SampleControlPoint = samplePoint;
+            Kiai = controlPointInfo.EffectPointAt(StartTime + control_point_leniency).KiaiMode;
 
             if (HitWindows == null)
                 HitWindows = CreateHitWindows();
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs
index 13fe021f95..3184f776a7 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs
@@ -116,12 +116,12 @@ namespace osu.Game.Scoring.Legacy
 
         private void calculateAccuracy(ScoreInfo score)
         {
-            int countMiss = (int)score.Statistics[HitResult.Miss];
-            int count50 = (int)score.Statistics[HitResult.Meh];
-            int count100 = (int)score.Statistics[HitResult.Good];
-            int count300 = (int)score.Statistics[HitResult.Great];
-            int countGeki = (int)score.Statistics[HitResult.Perfect];
-            int countKatu = (int)score.Statistics[HitResult.Ok];
+            int countMiss = score.Statistics[HitResult.Miss];
+            int count50 = score.Statistics[HitResult.Meh];
+            int count100 = score.Statistics[HitResult.Good];
+            int count300 = score.Statistics[HitResult.Great];
+            int countGeki = score.Statistics[HitResult.Perfect];
+            int countKatu = score.Statistics[HitResult.Ok];
 
             switch (score.Ruleset.ID)
             {
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index e6bab194b0..fb894e621e 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -104,7 +104,7 @@ namespace osu.Game.Scoring
         public DateTimeOffset Date { get; set; }
 
         [JsonIgnore]
-        public Dictionary<HitResult, object> Statistics = new Dictionary<HitResult, object>();
+        public Dictionary<HitResult, int> Statistics = new Dictionary<HitResult, int>();
 
         [Column("Statistics")]
         public string StatisticsJson
@@ -118,7 +118,7 @@ namespace osu.Game.Scoring
                     return;
                 }
 
-                Statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, object>>(value);
+                Statistics = JsonConvert.DeserializeObject<Dictionary<HitResult, int>>(value);
             }
         }
 
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 98be0871a1..11cee98bdf 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Screens.Play
             Add(content = new Container
             {
                 RelativeSizeAxes = Axes.Both,
-
+                AlwaysPresent = true, // The hud may be hidden but certain elements may need to still be updated
                 Children = new Drawable[]
                 {
                     ComboCounter = CreateComboCounter(),
diff --git a/osu.Game/Screens/Ranking/ResultsPageScore.cs b/osu.Game/Screens/Ranking/ResultsPageScore.cs
index 153d154d40..62103314e1 100644
--- a/osu.Game/Screens/Ranking/ResultsPageScore.cs
+++ b/osu.Game/Screens/Ranking/ResultsPageScore.cs
@@ -196,9 +196,9 @@ namespace osu.Game.Screens.Ranking
 
         private class DrawableScoreStatistic : Container
         {
-            private readonly KeyValuePair<HitResult, object> statistic;
+            private readonly KeyValuePair<HitResult, int> statistic;
 
-            public DrawableScoreStatistic(KeyValuePair<HitResult, object> statistic)
+            public DrawableScoreStatistic(KeyValuePair<HitResult, int> statistic)
             {
                 this.statistic = statistic;
 
diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs
index b5d333aee4..0e3dfcf284 100644
--- a/osu.Game/Screens/Select/PlaySongSelect.cs
+++ b/osu.Game/Screens/Select/PlaySongSelect.cs
@@ -85,6 +85,17 @@ namespace osu.Game.Screens.Select
             }
         }
 
+        protected override void ExitFromBack()
+        {
+            if (modSelect.State == Visibility.Visible)
+            {
+                modSelect.Hide();
+                return;
+            }
+
+            base.ExitFromBack();
+        }
+
         protected override void UpdateBeatmap(WorkingBeatmap beatmap)
         {
             beatmap.Mods.BindTo(selectedMods);
@@ -124,12 +135,6 @@ namespace osu.Game.Screens.Select
 
         protected override bool OnExiting(Screen next)
         {
-            if (modSelect.State == Visibility.Visible)
-            {
-                modSelect.Hide();
-                return true;
-            }
-
             if (base.OnExiting(next))
                 return true;
 
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index f4af4f9068..66540c6900 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -191,13 +191,15 @@ namespace osu.Game.Screens.Select
                 });
                 Add(Footer = new Footer
                 {
-                    OnBack = Exit,
+                    OnBack = ExitFromBack,
                 });
 
                 FooterPanels.Add(BeatmapOptions = new BeatmapOptionsOverlay());
             }
         }
 
+        protected virtual void ExitFromBack() => Exit();
+
         [BackgroundDependencyLoader(true)]
         private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuColour colours)
         {
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index cdb94d6ab7..1be75ebccc 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -14,10 +14,10 @@
     <ProjectReference Include="..\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="Humanizer" Version="2.5.1" />
+    <PackageReference Include="Humanizer" Version="2.5.16" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />
-    <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.0" />
+    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
     <PackageReference Include="ppy.osu.Framework" Version="0.0.7654" />
     <PackageReference Include="SharpCompress" Version="0.22.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />