From 5073bce2dcbf8a5bce4f33b06036822524494082 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 9 Aug 2019 10:47:52 +0300
Subject: [PATCH 01/29] Basic request implementation

---
 .../Requests/GetUserKudosuHistoryRequest.cs   | 21 +++++++++++++++++++
 .../Requests/Responses/APIKudosuHistory.cs    | 20 ++++++++++++++++++
 2 files changed, 41 insertions(+)
 create mode 100644 osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
 create mode 100644 osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs

diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
new file mode 100644
index 0000000000..e90e297672
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+    public class GetUserKudosuHistoryRequest : PaginatedAPIRequest<List<APIKudosuHistory>>
+    {
+        private readonly long userId;
+
+        public GetUserKudosuHistoryRequest(long userId, int page = 0, int itemsPerPage = 5)
+            : base(page, itemsPerPage)
+        {
+            this.userId = userId;
+        }
+
+        protected override string Target => $"users/{userId}/kudosu";
+    }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
new file mode 100644
index 0000000000..949ce200d2
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+    public class APIKudosuHistory
+    {
+        [JsonProperty("id")]
+        public int ID;
+
+        [JsonProperty("createdAt")]
+        public DateTimeOffset CreatedAt;
+
+        [JsonProperty("amount")]
+        public int Amount;
+    }
+}

From cf2a9db8e048abc2e788c8ee210a2d7b958c9010 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 9 Aug 2019 11:14:38 +0300
Subject: [PATCH 02/29] Implement basic layout for kudosu history

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 56 +++++++++++++++++++
 .../Kudosu/PaginatedKudosuHistoryContainer.cs | 51 +++++++++++++++++
 .../Profile/Sections/KudosuSection.cs         |  4 +-
 3 files changed, 110 insertions(+), 1 deletion(-)
 create mode 100644 osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
 create mode 100644 osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
new file mode 100644
index 0000000000..1671073242
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+
+namespace osu.Game.Overlays.Profile.Sections.Kudosu
+{
+    public class DrawableKudosuHistoryItem : DrawableProfileRow
+    {
+        private readonly APIKudosuHistory historyItem;
+        private LinkFlowContainer content;
+
+        public DrawableKudosuHistoryItem(APIKudosuHistory historyItem)
+        {
+            this.historyItem = historyItem;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            LeftFlowContainer.Padding = new MarginPadding { Left = 10 };
+
+            LeftFlowContainer.Add(content = new LinkFlowContainer
+            {
+                AutoSizeAxes = Axes.Y,
+                RelativeSizeAxes = Axes.X,
+            });
+
+            RightFlowContainer.Add(new DrawableDate(historyItem.CreatedAt)
+            {
+                Font = OsuFont.GetFont(size: 13),
+                Colour = OsuColour.Gray(0xAA),
+                Anchor = Anchor.TopRight,
+                Origin = Anchor.TopRight,
+            });
+
+            var formatted = createMessage();
+
+            content.AddLinks(formatted.Text, formatted.Links);
+        }
+
+        protected override Drawable CreateLeftVisual() => new Container
+        {
+            RelativeSizeAxes = Axes.Y,
+            AutoSizeAxes = Axes.X,
+        };
+
+        private MessageFormatter.MessageFormatterResult createMessage() => MessageFormatter.FormatText($@"{historyItem.Amount}");
+    }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
new file mode 100644
index 0000000000..29b1d3c5aa
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Online.API.Requests;
+using osu.Game.Users;
+using System.Linq;
+using osu.Framework.Bindables;
+
+namespace osu.Game.Overlays.Profile.Sections.Kudosu
+{
+    public class PaginatedKudosuHistoryContainer : PaginatedContainer
+    {
+        private GetUserKudosuHistoryRequest request;
+
+        public PaginatedKudosuHistoryContainer(Bindable<User> user, string header, string missing)
+            : base(user, header, missing)
+        {
+            ItemsPerPage = 5;
+        }
+
+        protected override void ShowMore()
+        {
+            request = new GetUserKudosuHistoryRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
+            request.Success += items => Schedule(() =>
+            {
+                MoreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0);
+                MoreButton.IsLoading = false;
+
+                if (!items.Any() && VisiblePages == 1)
+                {
+                    MissingText.Show();
+                    return;
+                }
+
+                MissingText.Hide();
+
+                foreach (var item in items)
+                    ItemsContainer.Add(new DrawableKudosuHistoryItem(item));
+            });
+
+            Api.Queue(request);
+        }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+            request?.Cancel();
+        }
+    }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs
index a17b68933c..9ccce7d837 100644
--- a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs
+++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using osu.Framework.Graphics;
 using osu.Game.Overlays.Profile.Sections.Kudosu;
 
 namespace osu.Game.Overlays.Profile.Sections
@@ -13,9 +14,10 @@ namespace osu.Game.Overlays.Profile.Sections
 
         public KudosuSection()
         {
-            Children = new[]
+            Children = new Drawable[]
             {
                 new KudosuInfo(User),
+                new PaginatedKudosuHistoryContainer(User, null, @"This user hasn't received any kudosu!"),
             };
         }
     }

From 093359c13bc05d590e227a9db7615f4dc287be57 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 9 Aug 2019 11:19:14 +0300
Subject: [PATCH 03/29] fix incorrect json property

---
 osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
index 949ce200d2..271dcc320e 100644
--- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Online.API.Requests.Responses
         [JsonProperty("id")]
         public int ID;
 
-        [JsonProperty("createdAt")]
+        [JsonProperty("created_at")]
         public DateTimeOffset CreatedAt;
 
         [JsonProperty("amount")]

From 2393bbc69b15e1f3421f3d4b140393bbaa8462ac Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Sun, 18 Aug 2019 21:27:53 +0300
Subject: [PATCH 04/29] Expand APIKudosuHistory

---
 .../Requests/GetUserKudosuHistoryRequest.cs   | 10 ++++
 .../Requests/Responses/APIKudosuHistory.cs    | 46 +++++++++++++++++--
 2 files changed, 53 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
index e90e297672..af37bd4b51 100644
--- a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
@@ -18,4 +18,14 @@ namespace osu.Game.Online.API.Requests
 
         protected override string Target => $"users/{userId}/kudosu";
     }
+
+    public enum KudosuAction
+    {
+        Give,
+        VoteGive,
+        Reset,
+        VoteReset,
+        DenyKudosuReset,
+        Revoke,
+    }
 }
diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
index 271dcc320e..d02f71c339 100644
--- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -2,19 +2,59 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using Humanizer;
 using Newtonsoft.Json;
 
 namespace osu.Game.Online.API.Requests.Responses
 {
     public class APIKudosuHistory
     {
-        [JsonProperty("id")]
-        public int ID;
-
         [JsonProperty("created_at")]
         public DateTimeOffset CreatedAt;
 
         [JsonProperty("amount")]
+        private int amount
+        {
+            set => Amount = Math.Abs(value);
+        }
+
         public int Amount;
+
+        [JsonProperty("post")]
+        public ModdingPost Post;
+
+        public class ModdingPost
+        {
+            [JsonProperty("url")]
+            public string Url;
+
+            [JsonProperty("title")]
+            public string Title;
+        }
+
+        [JsonProperty("giver")]
+        public KudosuGiver Giver;
+
+        public class KudosuGiver
+        {
+            [JsonProperty("url")]
+            public string Url;
+
+            [JsonProperty("username")]
+            public string Username;
+        }
+
+        [JsonProperty("action")]
+        private string action
+        {
+            set
+            {
+                string parsed = value.Contains(".") ? value.Split('.')[0].Pascalize() + value.Split('.')[1].Pascalize() : value.Pascalize();
+
+                Action = (KudosuAction)Enum.Parse(typeof(KudosuAction), parsed);
+            }
+        }
+
+        public KudosuAction Action;
     }
 }

From be97804180f00c93818ea91b9afd58037e27c7dd Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Sun, 18 Aug 2019 21:28:07 +0300
Subject: [PATCH 05/29] Implement text formatting

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 42 ++++++++++++++++++-
 1 file changed, 41 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 1671073242..9b68131515 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.API.Requests;
 using osu.Game.Online.Chat;
 
 namespace osu.Game.Overlays.Profile.Sections.Kudosu
@@ -51,6 +52,45 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             AutoSizeAxes = Axes.X,
         };
 
-        private MessageFormatter.MessageFormatterResult createMessage() => MessageFormatter.FormatText($@"{historyItem.Amount}");
+        private MessageFormatter.MessageFormatterResult createMessage()
+        {
+            string postLinkTemplate() => $"[{historyItem.Post.Url} {historyItem.Post.Title}]";
+            string userLinkTemplate() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
+
+            string message;
+
+            switch (historyItem.Action)
+            {
+                case KudosuAction.Give:
+                    message = $"Received {historyItem.Amount} kudosu from {userLinkTemplate()} for a post at {postLinkTemplate()}";
+                    break;
+
+                case KudosuAction.VoteGive:
+                    message = $"Received {historyItem.Amount} kudosu from obtaining votes in modding post of {postLinkTemplate()}";
+                    break;
+
+                case KudosuAction.Reset:
+                    message = $"Kudosu reset by {userLinkTemplate()} for the post {postLinkTemplate()}";
+                    break;
+
+                case KudosuAction.VoteReset:
+                    message = $"Lost {historyItem.Amount} kudosu from losing votes in modding post of {postLinkTemplate()}";
+                    break;
+
+                case KudosuAction.DenyKudosuReset:
+                    message = $"Denied {historyItem.Amount} kudosu from modding post {postLinkTemplate()}";
+                    break;
+
+                case KudosuAction.Revoke:
+                    message = $"Denied kudosu by {userLinkTemplate()} for the post {postLinkTemplate()}";
+                    break;
+
+                default:
+                    message = string.Empty;
+                    break;
+            }
+
+            return MessageFormatter.FormatText(message);
+        }
     }
 }

From 4c9b621f43d9a702be1bd3149758a9e030f781bf Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Sun, 18 Aug 2019 22:01:36 +0300
Subject: [PATCH 06/29] Fix some user links can't be opened inside the game

---
 osu.Game/Online/Chat/MessageFormatter.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index 4aaffdd161..db26945ef3 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -122,6 +122,7 @@ namespace osu.Game.Online.Chat
                                 return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]);
 
                             case "u":
+                            case "users":
                                 return new LinkDetails(LinkAction.OpenUserProfile, args[3]);
                         }
                     }

From daeefc449c30785ba9db860ddaaf7f7fc7c92c18 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Mon, 19 Aug 2019 17:49:53 +0300
Subject: [PATCH 07/29] Use another link format to avoid representation issues

---
 .../Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs        | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 9b68131515..ac5801f989 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
 
         private MessageFormatter.MessageFormatterResult createMessage()
         {
-            string postLinkTemplate() => $"[{historyItem.Post.Url} {historyItem.Post.Title}]";
+            string postLinkTemplate() => $"({historyItem.Post.Title})[{historyItem.Post.Url}]";
             string userLinkTemplate() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
 
             string message;

From 6b8fbf0eb1ead2c25e04b8a62ca70aaaf8e42b2c Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Mon, 19 Aug 2019 17:59:55 +0300
Subject: [PATCH 08/29] Change link format back

Due to unavaliability to handle round brackets
---
 .../Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs        | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index ac5801f989..9b68131515 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
 
         private MessageFormatter.MessageFormatterResult createMessage()
         {
-            string postLinkTemplate() => $"({historyItem.Post.Title})[{historyItem.Post.Url}]";
+            string postLinkTemplate() => $"[{historyItem.Post.Url} {historyItem.Post.Title}]";
             string userLinkTemplate() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
 
             string message;

From f1d02d8169a6b38b6f8019c83a3fbaea8bae2d81 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 20 Aug 2019 15:00:14 +0300
Subject: [PATCH 09/29] Update design in line with web

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 105 +++++++++++++-----
 1 file changed, 78 insertions(+), 27 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 9b68131515..7c120e60f3 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -12,49 +12,100 @@ using osu.Game.Online.Chat;
 
 namespace osu.Game.Overlays.Profile.Sections.Kudosu
 {
-    public class DrawableKudosuHistoryItem : DrawableProfileRow
+    public class DrawableKudosuHistoryItem : CompositeDrawable
     {
+        private const int height = 25;
+
+        [Resolved]
+        private OsuColour colours { get; set; }
+
         private readonly APIKudosuHistory historyItem;
-        private LinkFlowContainer content;
+        private readonly LinkFlowContainer linkFlowContainer;
+        private readonly DrawableDate date;
 
         public DrawableKudosuHistoryItem(APIKudosuHistory historyItem)
         {
             this.historyItem = historyItem;
+
+            Height = height;
+            RelativeSizeAxes = Axes.X;
+            AddRangeInternal(new Drawable[]
+            {
+                linkFlowContainer = new LinkFlowContainer
+                {
+                    Anchor = Anchor.CentreLeft,
+                    Origin = Anchor.CentreLeft,
+                    AutoSizeAxes = Axes.Both,
+                },
+                date = new DrawableDate(historyItem.CreatedAt)
+                {
+                    Anchor = Anchor.CentreRight,
+                    Origin = Anchor.CentreRight,
+                }
+            });
         }
 
         [BackgroundDependencyLoader]
         private void load()
         {
-            LeftFlowContainer.Padding = new MarginPadding { Left = 10 };
+            date.Colour = colours.GreySeafoamLighter;
 
-            LeftFlowContainer.Add(content = new LinkFlowContainer
+            switch (historyItem.Action)
             {
-                AutoSizeAxes = Axes.Y,
-                RelativeSizeAxes = Axes.X,
-            });
+                case KudosuAction.VoteGive:
+                case KudosuAction.Give:
+                    linkFlowContainer.AddText($@"Received ");
+                    addKudosuPart();
+                    addMainPart();
+                    addPostPart();
+                    break;
 
-            RightFlowContainer.Add(new DrawableDate(historyItem.CreatedAt)
-            {
-                Font = OsuFont.GetFont(size: 13),
-                Colour = OsuColour.Gray(0xAA),
-                Anchor = Anchor.TopRight,
-                Origin = Anchor.TopRight,
-            });
+                case KudosuAction.Reset:
+                    addMainPart();
+                    addPostPart();
+                    break;
 
-            var formatted = createMessage();
+                case KudosuAction.VoteReset:
+                    linkFlowContainer.AddText($@"Lost ");
+                    addKudosuPart();
+                    addMainPart();
+                    addPostPart();
+                    break;
 
-            content.AddLinks(formatted.Text, formatted.Links);
+                case KudosuAction.DenyKudosuReset:
+                    linkFlowContainer.AddText($@"Denied ");
+                    addKudosuPart();
+                    addMainPart();
+                    addPostPart();
+                    break;
+
+                case KudosuAction.Revoke:
+                    addMainPart();
+                    addPostPart();
+                    break;
+            }
         }
 
-        protected override Drawable CreateLeftVisual() => new Container
+        private void addKudosuPart()
         {
-            RelativeSizeAxes = Axes.Y,
-            AutoSizeAxes = Axes.X,
-        };
+            linkFlowContainer.AddText($@"{historyItem.Amount} kudosu", t =>
+            {
+                t.Font = t.Font.With(italics: true);
+                t.Colour = colours.Blue;
+            });
+        }
+
+        private void addMainPart()
+        {
+            var text = createMessage();
+
+            linkFlowContainer.AddLinks(text.Text, text.Links);
+        }
+
+        private void addPostPart() => linkFlowContainer.AddLink(historyItem.Post.Title, historyItem.Post.Url);
 
         private MessageFormatter.MessageFormatterResult createMessage()
         {
-            string postLinkTemplate() => $"[{historyItem.Post.Url} {historyItem.Post.Title}]";
             string userLinkTemplate() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
 
             string message;
@@ -62,27 +113,27 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             switch (historyItem.Action)
             {
                 case KudosuAction.Give:
-                    message = $"Received {historyItem.Amount} kudosu from {userLinkTemplate()} for a post at {postLinkTemplate()}";
+                    message = $" from {userLinkTemplate()} for a post at ";
                     break;
 
                 case KudosuAction.VoteGive:
-                    message = $"Received {historyItem.Amount} kudosu from obtaining votes in modding post of {postLinkTemplate()}";
+                    message = $" from obtaining votes in modding post of ";
                     break;
 
                 case KudosuAction.Reset:
-                    message = $"Kudosu reset by {userLinkTemplate()} for the post {postLinkTemplate()}";
+                    message = $"Kudosu reset by {userLinkTemplate()} for the post ";
                     break;
 
                 case KudosuAction.VoteReset:
-                    message = $"Lost {historyItem.Amount} kudosu from losing votes in modding post of {postLinkTemplate()}";
+                    message = $" from losing votes in modding post of ";
                     break;
 
                 case KudosuAction.DenyKudosuReset:
-                    message = $"Denied {historyItem.Amount} kudosu from modding post {postLinkTemplate()}";
+                    message = $" from modding post ";
                     break;
 
                 case KudosuAction.Revoke:
-                    message = $"Denied kudosu by {userLinkTemplate()} for the post {postLinkTemplate()}";
+                    message = $"Denied kudosu by {userLinkTemplate()} for the post ";
                     break;
 
                 default:

From 832b365bd08ac76da29707aea9d2d466d745497c Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 20 Aug 2019 15:17:31 +0300
Subject: [PATCH 10/29] Add testing

---
 .../Visual/Online/TestSceneKudosuHistory.cs   | 156 ++++++++++++++++++
 1 file changed, 156 insertions(+)
 create mode 100644 osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs

diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
new file mode 100644
index 0000000000..6424db89fa
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -0,0 +1,156 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Overlays.Profile.Sections.Kudosu;
+using System.Collections.Generic;
+using System;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.API.Requests;
+using osu.Framework.Extensions.IEnumerableExtensions;
+
+namespace osu.Game.Tests.Visual.Online
+{
+    public class TestSceneKudosuHistory : OsuTestScene
+    {
+        public override IReadOnlyList<Type> RequiredTypes => new[]
+        {
+            typeof(DrawableKudosuHistoryItem),
+        };
+
+        private readonly Box background;
+
+        public TestSceneKudosuHistory()
+        {
+            FillFlowContainer<DrawableKudosuHistoryItem> content;
+
+            AddRange(new Drawable[]
+            {
+                background = new Box
+                {
+                    RelativeSizeAxes = Axes.Both,
+                },
+                content = new FillFlowContainer<DrawableKudosuHistoryItem>
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.X,
+                    Width = 0.7f,
+                    AutoSizeAxes = Axes.Y,
+                }
+            });
+
+            items.ForEach(t => content.Add(new DrawableKudosuHistoryItem(t)));
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            background.Colour = colours.GreySeafoam;
+        }
+
+        private IEnumerable<APIKudosuHistory> items = new[]
+        {
+            new APIKudosuHistory
+            {
+                Amount = 10,
+                CreatedAt = new DateTimeOffset(new DateTime(2011, 11, 11)),
+                Action = KudosuAction.DenyKudosuReset,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 1",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username1",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 5,
+                CreatedAt = new DateTimeOffset(new DateTime(2012, 10, 11)),
+                Action = KudosuAction.Give,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 2",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username2",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 8,
+                CreatedAt = new DateTimeOffset(new DateTime(2013, 9, 11)),
+                Action = KudosuAction.Reset,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 3",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username3",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 7,
+                CreatedAt = new DateTimeOffset(new DateTime(2014, 8, 11)),
+                Action = KudosuAction.Revoke,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 4",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username4",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 100,
+                CreatedAt = new DateTimeOffset(new DateTime(2015, 7, 11)),
+                Action = KudosuAction.VoteGive,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 5",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username5",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 20,
+                CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
+                Action = KudosuAction.VoteReset,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 6",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username6",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            }
+        };
+    }
+}

From c4344f3f7cc985b5f40ad4e1603c59ab910b3e2b Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 20 Aug 2019 15:29:32 +0300
Subject: [PATCH 11/29] CI fixes

---
 .../Visual/Online/TestSceneKudosuHistory.cs    |  2 +-
 .../Kudosu/DrawableKudosuHistoryItem.cs        | 18 +++++++++---------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
index 6424db89fa..dcf2bec239 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Online
             background.Colour = colours.GreySeafoam;
         }
 
-        private IEnumerable<APIKudosuHistory> items = new[]
+        private readonly IEnumerable<APIKudosuHistory> items = new[]
         {
             new APIKudosuHistory
             {
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 7c120e60f3..d6dfdc84ec 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             {
                 case KudosuAction.VoteGive:
                 case KudosuAction.Give:
-                    linkFlowContainer.AddText($@"Received ");
+                    linkFlowContainer.AddText(@"Received ");
                     addKudosuPart();
                     addMainPart();
                     addPostPart();
@@ -66,14 +66,14 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                     break;
 
                 case KudosuAction.VoteReset:
-                    linkFlowContainer.AddText($@"Lost ");
+                    linkFlowContainer.AddText(@"Lost ");
                     addKudosuPart();
                     addMainPart();
                     addPostPart();
                     break;
 
                 case KudosuAction.DenyKudosuReset:
-                    linkFlowContainer.AddText($@"Denied ");
+                    linkFlowContainer.AddText(@"Denied ");
                     addKudosuPart();
                     addMainPart();
                     addPostPart();
@@ -113,27 +113,27 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             switch (historyItem.Action)
             {
                 case KudosuAction.Give:
-                    message = $" from {userLinkTemplate()} for a post at ";
+                    message = $@" from {userLinkTemplate()} for a post at ";
                     break;
 
                 case KudosuAction.VoteGive:
-                    message = $" from obtaining votes in modding post of ";
+                    message = @" from obtaining votes in modding post of ";
                     break;
 
                 case KudosuAction.Reset:
-                    message = $"Kudosu reset by {userLinkTemplate()} for the post ";
+                    message = $@"Kudosu reset by {userLinkTemplate()} for the post ";
                     break;
 
                 case KudosuAction.VoteReset:
-                    message = $" from losing votes in modding post of ";
+                    message = @" from losing votes in modding post of ";
                     break;
 
                 case KudosuAction.DenyKudosuReset:
-                    message = $" from modding post ";
+                    message = @" from modding post ";
                     break;
 
                 case KudosuAction.Revoke:
-                    message = $"Denied kudosu by {userLinkTemplate()} for the post ";
+                    message = $@"Denied kudosu by {userLinkTemplate()} for the post ";
                     break;
 
                 default:

From 5c7cb4dc21ecb4a3b5961a4bed7c7f4b1562aa1a Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 20 Aug 2019 16:11:59 +0300
Subject: [PATCH 12/29] Simplify text creation

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 64 +++++--------------
 1 file changed, 16 insertions(+), 48 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index d6dfdc84ec..1b0a501db2 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -50,37 +50,45 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
         {
             date.Colour = colours.GreySeafoamLighter;
 
+            string userLinkTemplate() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
+
             switch (historyItem.Action)
             {
                 case KudosuAction.VoteGive:
+                    linkFlowContainer.AddText(@"Received ");
+                    addKudosuPart();
+                    addMainPart($@" from {userLinkTemplate()} for a post at ");
+                    addPostPart();
+                    break;
+
                 case KudosuAction.Give:
                     linkFlowContainer.AddText(@"Received ");
                     addKudosuPart();
-                    addMainPart();
+                    addMainPart(@" from obtaining votes in modding post of ");
                     addPostPart();
                     break;
 
                 case KudosuAction.Reset:
-                    addMainPart();
+                    addMainPart($@"Kudosu reset by {userLinkTemplate()} for the post ");
                     addPostPart();
                     break;
 
                 case KudosuAction.VoteReset:
                     linkFlowContainer.AddText(@"Lost ");
                     addKudosuPart();
-                    addMainPart();
+                    addMainPart(@" from losing votes in modding post of ");
                     addPostPart();
                     break;
 
                 case KudosuAction.DenyKudosuReset:
                     linkFlowContainer.AddText(@"Denied ");
                     addKudosuPart();
-                    addMainPart();
+                    addMainPart(@" from modding post ");
                     addPostPart();
                     break;
 
                 case KudosuAction.Revoke:
-                    addMainPart();
+                    addMainPart($@"Denied kudosu by {userLinkTemplate()} for the post ");
                     addPostPart();
                     break;
             }
@@ -95,53 +103,13 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             });
         }
 
-        private void addMainPart()
+        private void addMainPart(string text)
         {
-            var text = createMessage();
+            var formatted = MessageFormatter.FormatText(text);
 
-            linkFlowContainer.AddLinks(text.Text, text.Links);
+            linkFlowContainer.AddLinks(formatted.Text, formatted.Links);
         }
 
         private void addPostPart() => linkFlowContainer.AddLink(historyItem.Post.Title, historyItem.Post.Url);
-
-        private MessageFormatter.MessageFormatterResult createMessage()
-        {
-            string userLinkTemplate() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
-
-            string message;
-
-            switch (historyItem.Action)
-            {
-                case KudosuAction.Give:
-                    message = $@" from {userLinkTemplate()} for a post at ";
-                    break;
-
-                case KudosuAction.VoteGive:
-                    message = @" from obtaining votes in modding post of ";
-                    break;
-
-                case KudosuAction.Reset:
-                    message = $@"Kudosu reset by {userLinkTemplate()} for the post ";
-                    break;
-
-                case KudosuAction.VoteReset:
-                    message = @" from losing votes in modding post of ";
-                    break;
-
-                case KudosuAction.DenyKudosuReset:
-                    message = @" from modding post ";
-                    break;
-
-                case KudosuAction.Revoke:
-                    message = $@"Denied kudosu by {userLinkTemplate()} for the post ";
-                    break;
-
-                default:
-                    message = string.Empty;
-                    break;
-            }
-
-            return MessageFormatter.FormatText(message);
-        }
     }
 }

From 426c7a48989302453ca7e05b8c91738e06d6ad7e Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 20 Aug 2019 16:19:21 +0300
Subject: [PATCH 13/29] Fix incorrect templates

---
 .../Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 1b0a501db2..e5f5b720c2 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -57,14 +57,14 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                 case KudosuAction.VoteGive:
                     linkFlowContainer.AddText(@"Received ");
                     addKudosuPart();
-                    addMainPart($@" from {userLinkTemplate()} for a post at ");
+                    addMainPart(@" from obtaining votes in modding post of ");
                     addPostPart();
                     break;
 
                 case KudosuAction.Give:
                     linkFlowContainer.AddText(@"Received ");
                     addKudosuPart();
-                    addMainPart(@" from obtaining votes in modding post of ");
+                    addMainPart($@" from {userLinkTemplate()} for a post at ");
                     addPostPart();
                     break;
 

From f4d2bb036b3bcd9ed7819ad1380e1310bf8a2984 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Thu, 22 Aug 2019 16:50:54 +0300
Subject: [PATCH 14/29] Expand KudosuAction list

---
 .../Visual/Online/TestSceneKudosuHistory.cs   | 80 +++++++++++++++++++
 .../Requests/GetUserKudosuHistoryRequest.cs   |  5 ++
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 35 ++++++++
 3 files changed, 120 insertions(+)

diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
index dcf2bec239..8badfeaa23 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -150,6 +150,86 @@ namespace osu.Game.Tests.Visual.Online
                     Username = @"Username6",
                     Url = @"https://osu.ppy.sh/u/1234"
                 }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 11,
+                CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
+                Action = KudosuAction.AllowKudosuGive,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 7",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username7",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 24,
+                CreatedAt = new DateTimeOffset(new DateTime(2014, 6, 11)),
+                Action = KudosuAction.DeleteReset,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 8",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username8",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 12,
+                CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
+                Action = KudosuAction.RestoreGive,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 9",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username9",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 2,
+                CreatedAt = new DateTimeOffset(new DateTime(2012, 6, 11)),
+                Action = KudosuAction.RecalculateGive,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 10",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username10",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
+            },
+            new APIKudosuHistory
+            {
+                Amount = 32,
+                CreatedAt = new DateTimeOffset(new DateTime(2019, 8, 11)),
+                Action = KudosuAction.RecalculateReset,
+                Post = new APIKudosuHistory.ModdingPost
+                {
+                    Title = @"Random post 11",
+                    Url = @"https://osu.ppy.sh/b/1234",
+                },
+                Giver = new APIKudosuHistory.KudosuGiver
+                {
+                    Username = @"Username11",
+                    Url = @"https://osu.ppy.sh/u/1234"
+                }
             }
         };
     }
diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
index af37bd4b51..dd6f2ccf22 100644
--- a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
@@ -27,5 +27,10 @@ namespace osu.Game.Online.API.Requests
         VoteReset,
         DenyKudosuReset,
         Revoke,
+        AllowKudosuGive,
+        DeleteReset,
+        RestoreGive,
+        RecalculateGive,
+        RecalculateReset
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index e5f5b720c2..3cc39f0e73 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -91,6 +91,41 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                     addMainPart($@"Denied kudosu by {userLinkTemplate()} for the post ");
                     addPostPart();
                     break;
+
+                case KudosuAction.AllowKudosuGive:
+                    linkFlowContainer.AddText(@"Received ");
+                    addKudosuPart();
+                    addMainPart(@" from kudosu deny repeal of modding post ");
+                    addPostPart();
+                    break;
+
+                case KudosuAction.DeleteReset:
+                    linkFlowContainer.AddText(@"Lost ");
+                    addKudosuPart();
+                    addMainPart(@" from modding post deletion of ");
+                    addPostPart();
+                    break;
+
+                case KudosuAction.RestoreGive:
+                    linkFlowContainer.AddText(@"Received ");
+                    addKudosuPart();
+                    addMainPart(@" from modding post restoration of ");
+                    addPostPart();
+                    break;
+
+                case KudosuAction.RecalculateGive:
+                    linkFlowContainer.AddText(@"Received ");
+                    addKudosuPart();
+                    addMainPart(@" from votes recalculation in modding post of ");
+                    addPostPart();
+                    break;
+
+                case KudosuAction.RecalculateReset:
+                    linkFlowContainer.AddText(@"Lost ");
+                    addKudosuPart();
+                    addMainPart(@" from votes recalculation in modding post of ");
+                    addPostPart();
+                    break;
             }
         }
 

From 050130e1591e6ec6991b9027fa51c0d085498307 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 23 Aug 2019 14:11:21 +0300
Subject: [PATCH 15/29] Refactor PaginatedContainer to centralise repetitive
 logic

---
 .../Beatmaps/PaginatedBeatmapContainer.cs     | 48 +++----------
 .../PaginatedMostPlayedBeatmapContainer.cs    | 41 +++--------
 .../Kudosu/PaginatedKudosuHistoryContainer.cs | 38 ++--------
 .../Profile/Sections/PaginatedContainer.cs    | 72 +++++++++++++++----
 .../Sections/Ranks/PaginatedScoreContainer.cs | 65 ++++++-----------
 .../PaginatedRecentActivityContainer.cs       | 39 ++--------
 6 files changed, 110 insertions(+), 193 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
index 8a6b52b7ee..3fbd1dacd9 100644
--- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
@@ -1,21 +1,22 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.Linq;
+using System.Collections.Generic;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
+using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Overlays.Direct;
 using osu.Game.Users;
 using osuTK;
 
 namespace osu.Game.Overlays.Profile.Sections.Beatmaps
 {
-    public class PaginatedBeatmapContainer : PaginatedContainer
+    public class PaginatedBeatmapContainer : PaginatedContainer<APIBeatmapSet>
     {
         private const float panel_padding = 10f;
         private readonly BeatmapSetType type;
-        private GetUserBeatmapsRequest request;
 
         public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<User> user, string header, string missing = "None... yet.")
             : base(user, header, missing)
@@ -27,40 +28,13 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
             ItemsContainer.Spacing = new Vector2(panel_padding);
         }
 
-        protected override void ShowMore()
+        protected override APIRequest<List<APIBeatmapSet>> CreateRequest()
+            => new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
+
+        protected override Drawable CreateDrawableItem(APIBeatmapSet item) => new DirectGridPanel(item.ToBeatmapSet(Rulesets))
         {
-            request = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
-            request.Success += sets => Schedule(() =>
-            {
-                MoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0);
-                MoreButton.IsLoading = false;
-
-                if (!sets.Any() && VisiblePages == 1)
-                {
-                    MissingText.Show();
-                    return;
-                }
-
-                foreach (var s in sets)
-                {
-                    if (!s.OnlineBeatmapSetID.HasValue)
-                        continue;
-
-                    ItemsContainer.Add(new DirectGridPanel(s.ToBeatmapSet(Rulesets))
-                    {
-                        Anchor = Anchor.TopCentre,
-                        Origin = Anchor.TopCentre,
-                    });
-                }
-            });
-
-            Api.Queue(request);
-        }
-
-        protected override void Dispose(bool isDisposing)
-        {
-            base.Dispose(isDisposing);
-            request?.Cancel();
-        }
+            Anchor = Anchor.TopCentre,
+            Origin = Anchor.TopCentre,
+        };
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
index 23072f8d90..e444363e52 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
@@ -1,19 +1,19 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.Linq;
+using System.Collections.Generic;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Users;
 
 namespace osu.Game.Overlays.Profile.Sections.Historical
 {
-    public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer
+    public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer<APIUserMostPlayedBeatmap>
     {
-        private GetUserMostPlayedBeatmapsRequest request;
-
         public PaginatedMostPlayedBeatmapContainer(Bindable<User> user)
             : base(user, "Most Played Beatmaps", "No records. :(")
         {
@@ -22,35 +22,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
             ItemsContainer.Direction = FillDirection.Vertical;
         }
 
-        protected override void ShowMore()
-        {
-            request = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
-            request.Success += beatmaps => Schedule(() =>
-            {
-                MoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0);
-                MoreButton.IsLoading = false;
+        protected override APIRequest<List<APIUserMostPlayedBeatmap>> CreateRequest()
+            => new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
 
-                if (!beatmaps.Any() && VisiblePages == 1)
-                {
-                    MissingText.Show();
-                    return;
-                }
-
-                MissingText.Hide();
-
-                foreach (var beatmap in beatmaps)
-                {
-                    ItemsContainer.Add(new DrawableMostPlayedBeatmap(beatmap.GetBeatmapInfo(Rulesets), beatmap.PlayCount));
-                }
-            });
-
-            Api.Queue(request);
-        }
-
-        protected override void Dispose(bool isDisposing)
-        {
-            base.Dispose(isDisposing);
-            request?.Cancel();
-        }
+        protected override Drawable CreateDrawableItem(APIUserMostPlayedBeatmap item)
+            => new DrawableMostPlayedBeatmap(item.GetBeatmapInfo(Rulesets), item.PlayCount);
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
index 29b1d3c5aa..0e7cfc37c0 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
@@ -4,48 +4,24 @@
 using osu.Framework.Graphics;
 using osu.Game.Online.API.Requests;
 using osu.Game.Users;
-using System.Linq;
 using osu.Framework.Bindables;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.API;
+using System.Collections.Generic;
 
 namespace osu.Game.Overlays.Profile.Sections.Kudosu
 {
-    public class PaginatedKudosuHistoryContainer : PaginatedContainer
+    public class PaginatedKudosuHistoryContainer : PaginatedContainer<APIKudosuHistory>
     {
-        private GetUserKudosuHistoryRequest request;
-
         public PaginatedKudosuHistoryContainer(Bindable<User> user, string header, string missing)
             : base(user, header, missing)
         {
             ItemsPerPage = 5;
         }
 
-        protected override void ShowMore()
-        {
-            request = new GetUserKudosuHistoryRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
-            request.Success += items => Schedule(() =>
-            {
-                MoreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0);
-                MoreButton.IsLoading = false;
+        protected override APIRequest<List<APIKudosuHistory>> CreateRequest()
+            => new GetUserKudosuHistoryRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
 
-                if (!items.Any() && VisiblePages == 1)
-                {
-                    MissingText.Show();
-                    return;
-                }
-
-                MissingText.Hide();
-
-                foreach (var item in items)
-                    ItemsContainer.Add(new DrawableKudosuHistoryItem(item));
-            });
-
-            Api.Queue(request);
-        }
-
-        protected override void Dispose(bool isDisposing)
-        {
-            base.Dispose(isDisposing);
-            request?.Cancel();
-        }
+        protected override Drawable CreateDrawableItem(APIKudosuHistory item) => new DrawableKudosuHistoryItem(item);
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index b459afcb49..e329fce67b 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -11,22 +11,25 @@ using osu.Game.Graphics.Sprites;
 using osu.Game.Online.API;
 using osu.Game.Rulesets;
 using osu.Game.Users;
+using System.Collections.Generic;
+using System.Linq;
 
 namespace osu.Game.Overlays.Profile.Sections
 {
-    public abstract class PaginatedContainer : FillFlowContainer
+    public abstract class PaginatedContainer<T> : FillFlowContainer
     {
-        protected readonly FillFlowContainer ItemsContainer;
-        protected readonly ShowMoreButton MoreButton;
-        protected readonly OsuSpriteText MissingText;
+        private readonly ShowMoreButton moreButton;
+        private readonly OsuSpriteText missingText;
+        private APIRequest<List<T>> retrievalRequest;
+
+        [Resolved]
+        private IAPIProvider api { get; set; }
 
         protected int VisiblePages;
         protected int ItemsPerPage;
 
         protected readonly Bindable<User> User = new Bindable<User>();
-
-        protected IAPIProvider Api;
-        protected APIRequest RetrievalRequest;
+        protected readonly FillFlowContainer ItemsContainer;
         protected RulesetStore Rulesets;
 
         protected PaginatedContainer(Bindable<User> user, string header, string missing)
@@ -51,15 +54,15 @@ namespace osu.Game.Overlays.Profile.Sections
                     RelativeSizeAxes = Axes.X,
                     Spacing = new Vector2(0, 2),
                 },
-                MoreButton = new ShowMoreButton
+                moreButton = new ShowMoreButton
                 {
                     Anchor = Anchor.TopCentre,
                     Origin = Anchor.TopCentre,
                     Alpha = 0,
                     Margin = new MarginPadding { Top = 10 },
-                    Action = ShowMore,
+                    Action = showMore,
                 },
-                MissingText = new OsuSpriteText
+                missingText = new OsuSpriteText
                 {
                     Font = OsuFont.GetFont(size: 15),
                     Text = missing,
@@ -69,9 +72,8 @@ namespace osu.Game.Overlays.Profile.Sections
         }
 
         [BackgroundDependencyLoader]
-        private void load(IAPIProvider api, RulesetStore rulesets)
+        private void load(RulesetStore rulesets)
         {
-            Api = api;
             Rulesets = rulesets;
 
             User.ValueChanged += onUserChanged;
@@ -84,9 +86,51 @@ namespace osu.Game.Overlays.Profile.Sections
             ItemsContainer.Clear();
 
             if (e.NewValue != null)
-                ShowMore();
+                showMore();
         }
 
-        protected abstract void ShowMore();
+        private void showMore()
+        {
+            retrievalRequest = CreateRequest();
+            retrievalRequest.Success += items => UpdateItems(items);
+
+            api.Queue(retrievalRequest);
+        }
+
+        protected virtual void UpdateItems(List<T> items)
+        {
+            Schedule(() =>
+            {
+                moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0);
+                moreButton.IsLoading = false;
+
+                if (!items.Any() && VisiblePages == 1)
+                {
+                    moreButton.Hide();
+                    moreButton.IsLoading = false;
+                    missingText.Show();
+                    return;
+                }
+
+                LoadComponentsAsync(items.Select(item => CreateDrawableItem(item)), i =>
+                {
+                    missingText.Hide();
+                    moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0);
+                    moreButton.IsLoading = false;
+
+                    ItemsContainer.AddRange(i);
+                });
+            });
+        }
+
+        protected abstract APIRequest<List<T>> CreateRequest();
+
+        protected abstract Drawable CreateDrawableItem(T item);
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+            retrievalRequest?.Cancel();
+        }
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index 4a9ac6e5c7..dfe300b069 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -5,18 +5,18 @@ 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.Framework.Bindables;
 using osu.Framework.Graphics;
+using osu.Game.Online.API.Requests.Responses;
+using System.Collections.Generic;
+using osu.Game.Online.API;
 
 namespace osu.Game.Overlays.Profile.Sections.Ranks
 {
-    public class PaginatedScoreContainer : PaginatedContainer
+    public class PaginatedScoreContainer : PaginatedContainer<APILegacyScoreInfo>
     {
         private readonly bool includeWeight;
         private readonly ScoreType type;
-        private GetUserScoresRequest request;
 
         public PaginatedScoreContainer(ScoreType type, Bindable<User> user, string header, string missing, bool includeWeight = false)
             : base(user, header, missing)
@@ -29,52 +29,27 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
             ItemsContainer.Direction = FillDirection.Vertical;
         }
 
-        protected override void ShowMore()
+        protected override void UpdateItems(List<APILegacyScoreInfo> items)
         {
-            request = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
-            request.Success += scores => Schedule(() =>
-            {
-                foreach (var s in scores)
-                    s.Ruleset = Rulesets.GetRuleset(s.RulesetID);
+            foreach (var item in items)
+                item.Ruleset = Rulesets.GetRuleset(item.RulesetID);
 
-                if (!scores.Any() && VisiblePages == 1)
-                {
-                    MoreButton.Hide();
-                    MoreButton.IsLoading = false;
-                    MissingText.Show();
-                    return;
-                }
-
-                IEnumerable<DrawableProfileScore> drawableScores;
-
-                switch (type)
-                {
-                    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();
-                    MoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0);
-                    MoreButton.IsLoading = false;
-
-                    ItemsContainer.AddRange(s);
-                });
-            });
-
-            Api.Queue(request);
+            base.UpdateItems(items);
         }
 
-        protected override void Dispose(bool isDisposing)
+        protected override APIRequest<List<APILegacyScoreInfo>> CreateRequest()
+            => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
+
+        protected override Drawable CreateDrawableItem(APILegacyScoreInfo item)
         {
-            base.Dispose(isDisposing);
-            request?.Cancel();
+            switch (type)
+            {
+                default:
+                    return new DrawablePerformanceScore(item, includeWeight ? Math.Pow(0.95, ItemsContainer.Count) : (double?)null);
+
+                case ScoreType.Recent:
+                    return new DrawableTotalScore(item);
+            }
         }
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
index f2a778a874..0251dd0740 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
@@ -4,51 +4,24 @@
 using osu.Framework.Graphics;
 using osu.Game.Online.API.Requests;
 using osu.Game.Users;
-using System.Linq;
 using osu.Framework.Bindables;
 using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.API;
+using System.Collections.Generic;
 
 namespace osu.Game.Overlays.Profile.Sections.Recent
 {
-    public class PaginatedRecentActivityContainer : PaginatedContainer
+    public class PaginatedRecentActivityContainer : PaginatedContainer<APIRecentActivity>
     {
-        private GetUserRecentActivitiesRequest request;
-
         public PaginatedRecentActivityContainer(Bindable<User> user, string header, string missing)
             : base(user, header, missing)
         {
             ItemsPerPage = 5;
         }
 
-        protected override void ShowMore()
-        {
-            request = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
-            request.Success += activities => Schedule(() =>
-            {
-                MoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0);
-                MoreButton.IsLoading = false;
+        protected override APIRequest<List<APIRecentActivity>> CreateRequest()
+            => new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
 
-                if (!activities.Any() && VisiblePages == 1)
-                {
-                    MissingText.Show();
-                    return;
-                }
-
-                MissingText.Hide();
-
-                foreach (APIRecentActivity activity in activities)
-                {
-                    ItemsContainer.Add(new DrawableRecentActivity(activity));
-                }
-            });
-
-            Api.Queue(request);
-        }
-
-        protected override void Dispose(bool isDisposing)
-        {
-            base.Dispose(isDisposing);
-            request?.Cancel();
-        }
+        protected override Drawable CreateDrawableItem(APIRecentActivity item) => new DrawableRecentActivity(item);
     }
 }

From 45c0826314fb89ec07026037aa93c7e50979cb80 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 23 Aug 2019 14:14:39 +0300
Subject: [PATCH 16/29] Remove repetitive code

---
 osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index e329fce67b..6c444b2e26 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -101,9 +101,6 @@ namespace osu.Game.Overlays.Profile.Sections
         {
             Schedule(() =>
             {
-                moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0);
-                moreButton.IsLoading = false;
-
                 if (!items.Any() && VisiblePages == 1)
                 {
                     moreButton.Hide();

From 7e34afeab874911ef0fd3edb1ad94388fc681d9d Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 23 Aug 2019 14:38:18 +0300
Subject: [PATCH 17/29] Conver to method group

---
 osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index 6c444b2e26..0e2b4c986a 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -82,6 +82,8 @@ namespace osu.Game.Overlays.Profile.Sections
 
         private void onUserChanged(ValueChangedEvent<User> e)
         {
+            retrievalRequest?.Cancel();
+
             VisiblePages = 0;
             ItemsContainer.Clear();
 
@@ -92,7 +94,7 @@ namespace osu.Game.Overlays.Profile.Sections
         private void showMore()
         {
             retrievalRequest = CreateRequest();
-            retrievalRequest.Success += items => UpdateItems(items);
+            retrievalRequest.Success += UpdateItems;
 
             api.Queue(retrievalRequest);
         }
@@ -109,13 +111,13 @@ namespace osu.Game.Overlays.Profile.Sections
                     return;
                 }
 
-                LoadComponentsAsync(items.Select(item => CreateDrawableItem(item)), i =>
+                LoadComponentsAsync(items.Select(CreateDrawableItem), drawables =>
                 {
                     missingText.Hide();
                     moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0);
                     moreButton.IsLoading = false;
 
-                    ItemsContainer.AddRange(i);
+                    ItemsContainer.AddRange(drawables);
                 });
             });
         }

From 0cde0982e595da7dc28240b78ed534e83f171507 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 23 Aug 2019 14:52:26 +0300
Subject: [PATCH 18/29] Use cansellation token

---
 osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index 0e2b4c986a..75601041e8 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets;
 using osu.Game.Users;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading;
 
 namespace osu.Game.Overlays.Profile.Sections
 {
@@ -21,6 +22,7 @@ namespace osu.Game.Overlays.Profile.Sections
         private readonly ShowMoreButton moreButton;
         private readonly OsuSpriteText missingText;
         private APIRequest<List<T>> retrievalRequest;
+        private CancellationTokenSource loadCancellation;
 
         [Resolved]
         private IAPIProvider api { get; set; }
@@ -82,6 +84,7 @@ namespace osu.Game.Overlays.Profile.Sections
 
         private void onUserChanged(ValueChangedEvent<User> e)
         {
+            loadCancellation?.Cancel();
             retrievalRequest?.Cancel();
 
             VisiblePages = 0;
@@ -93,6 +96,8 @@ namespace osu.Game.Overlays.Profile.Sections
 
         private void showMore()
         {
+            loadCancellation = new CancellationTokenSource();
+
             retrievalRequest = CreateRequest();
             retrievalRequest.Success += UpdateItems;
 
@@ -118,7 +123,7 @@ namespace osu.Game.Overlays.Profile.Sections
                     moreButton.IsLoading = false;
 
                     ItemsContainer.AddRange(drawables);
-                });
+                }, loadCancellation.Token);
             });
         }
 

From f49b58c102e641ada62971df2413b8e83e1aefb6 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 27 Aug 2019 15:30:41 +0300
Subject: [PATCH 19/29] Simplify text building

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 46 ++++++-------------
 1 file changed, 15 insertions(+), 31 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 3cc39f0e73..4dba07713f 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -55,83 +55,67 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             switch (historyItem.Action)
             {
                 case KudosuAction.VoteGive:
-                    linkFlowContainer.AddText(@"Received ");
-                    addKudosuPart();
+                    addKudosuPart(@"Received");
                     addMainPart(@" from obtaining votes in modding post of ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.Give:
-                    linkFlowContainer.AddText(@"Received ");
-                    addKudosuPart();
+                    addKudosuPart(@"Received");
                     addMainPart($@" from {userLinkTemplate()} for a post at ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.Reset:
                     addMainPart($@"Kudosu reset by {userLinkTemplate()} for the post ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.VoteReset:
-                    linkFlowContainer.AddText(@"Lost ");
-                    addKudosuPart();
+                    addKudosuPart(@"Lost");
                     addMainPart(@" from losing votes in modding post of ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.DenyKudosuReset:
-                    linkFlowContainer.AddText(@"Denied ");
-                    addKudosuPart();
+                    addKudosuPart(@"Denied");
                     addMainPart(@" from modding post ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.Revoke:
                     addMainPart($@"Denied kudosu by {userLinkTemplate()} for the post ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.AllowKudosuGive:
-                    linkFlowContainer.AddText(@"Received ");
-                    addKudosuPart();
+                    addKudosuPart(@"Received");
                     addMainPart(@" from kudosu deny repeal of modding post ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.DeleteReset:
-                    linkFlowContainer.AddText(@"Lost ");
-                    addKudosuPart();
+                    addKudosuPart(@"Lost");
                     addMainPart(@" from modding post deletion of ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.RestoreGive:
-                    linkFlowContainer.AddText(@"Received ");
-                    addKudosuPart();
+                    addKudosuPart(@"Received");
                     addMainPart(@" from modding post restoration of ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.RecalculateGive:
-                    linkFlowContainer.AddText(@"Received ");
-                    addKudosuPart();
+                    addKudosuPart(@"Received");
                     addMainPart(@" from votes recalculation in modding post of ");
-                    addPostPart();
                     break;
 
                 case KudosuAction.RecalculateReset:
-                    linkFlowContainer.AddText(@"Lost ");
-                    addKudosuPart();
+                    addKudosuPart(@"Lost");
                     addMainPart(@" from votes recalculation in modding post of ");
-                    addPostPart();
                     break;
             }
+
+            addPostPart();
         }
 
-        private void addKudosuPart()
+        private void addKudosuPart(string prefix)
         {
-            linkFlowContainer.AddText($@"{historyItem.Amount} kudosu", t =>
+            linkFlowContainer.AddText(prefix);
+
+            linkFlowContainer.AddText($@" {historyItem.Amount} kudosu", t =>
             {
                 t.Font = t.Font.With(italics: true);
                 t.Colour = colours.Blue;

From ed827d514f42d2d774f9b592dfee85c962036b94 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Tue, 27 Aug 2019 15:36:08 +0300
Subject: [PATCH 20/29] Add comments

---
 osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
index d02f71c339..19ce11aa13 100644
--- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Online.API.Requests.Responses
         [JsonProperty("amount")]
         private int amount
         {
+            //We can receive negative values. However "action" is enough to build needed items
             set => Amount = Math.Abs(value);
         }
 
@@ -49,6 +50,7 @@ namespace osu.Game.Online.API.Requests.Responses
         {
             set
             {
+                //We will receive something like "foo.bar" or just "foo"
                 string parsed = value.Contains(".") ? value.Split('.')[0].Pascalize() + value.Split('.')[1].Pascalize() : value.Pascalize();
 
                 Action = (KudosuAction)Enum.Parse(typeof(KudosuAction), parsed);

From bb22c2d6e48b8039b967bd7c7320f84e382af794 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 28 Aug 2019 17:29:18 +0900
Subject: [PATCH 21/29] Tidy up text construction

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 116 ++++++++++--------
 1 file changed, 66 insertions(+), 50 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 4dba07713f..fb7d597012 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -50,85 +50,101 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
         {
             date.Colour = colours.GreySeafoamLighter;
 
-            string userLinkTemplate() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
+            string prefix = getPrefix(historyItem);
+            var formattedSource = MessageFormatter.FormatText(getSource(historyItem));
+
+            if (!string.IsNullOrEmpty(prefix))
+            {
+                linkFlowContainer.AddText(prefix);
+                linkFlowContainer.AddText($@" {historyItem.Amount} kudosu", t =>
+                {
+                    t.Font = t.Font.With(italics: true);
+                    t.Colour = colours.Blue;
+                });
+            }
+
+            linkFlowContainer.AddLinks(formattedSource.Text + " ", formattedSource.Links);
+            linkFlowContainer.AddLink(historyItem.Post.Title, historyItem.Post.Url);
+        }
+
+        private string getSource(APIKudosuHistory historyItem)
+        {
+            string userLink() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
 
             switch (historyItem.Action)
             {
                 case KudosuAction.VoteGive:
-                    addKudosuPart(@"Received");
-                    addMainPart(@" from obtaining votes in modding post of ");
-                    break;
+                    return @" from obtaining votes in modding post of";
 
                 case KudosuAction.Give:
-                    addKudosuPart(@"Received");
-                    addMainPart($@" from {userLinkTemplate()} for a post at ");
-                    break;
+                    return $@" from {userLink()} for a post at";
 
                 case KudosuAction.Reset:
-                    addMainPart($@"Kudosu reset by {userLinkTemplate()} for the post ");
-                    break;
+                    return $@"Kudosu reset by {userLink()} for the post";
 
                 case KudosuAction.VoteReset:
-                    addKudosuPart(@"Lost");
-                    addMainPart(@" from losing votes in modding post of ");
-                    break;
+                    return @" from losing votes in modding post of";
 
                 case KudosuAction.DenyKudosuReset:
-                    addKudosuPart(@"Denied");
-                    addMainPart(@" from modding post ");
-                    break;
+                    return @" from modding post";
 
                 case KudosuAction.Revoke:
-                    addMainPart($@"Denied kudosu by {userLinkTemplate()} for the post ");
-                    break;
+                    return $@"Denied kudosu by {userLink()} for the post";
 
                 case KudosuAction.AllowKudosuGive:
-                    addKudosuPart(@"Received");
-                    addMainPart(@" from kudosu deny repeal of modding post ");
-                    break;
+                    return @" from kudosu deny repeal of modding post";
 
                 case KudosuAction.DeleteReset:
-                    addKudosuPart(@"Lost");
-                    addMainPart(@" from modding post deletion of ");
-                    break;
+                    return @" from modding post deletion of";
 
                 case KudosuAction.RestoreGive:
-                    addKudosuPart(@"Received");
-                    addMainPart(@" from modding post restoration of ");
-                    break;
+                    return @" from modding post restoration of";
 
                 case KudosuAction.RecalculateGive:
-                    addKudosuPart(@"Received");
-                    addMainPart(@" from votes recalculation in modding post of ");
-                    break;
+                    return @" from votes recalculation in modding post of";
 
                 case KudosuAction.RecalculateReset:
-                    addKudosuPart(@"Lost");
-                    addMainPart(@" from votes recalculation in modding post of ");
-                    break;
+                    return @" from votes recalculation in modding post of";
+
+                default:
+                    return @" from unknown event ";
             }
-
-            addPostPart();
         }
 
-        private void addKudosuPart(string prefix)
+        private string getPrefix(APIKudosuHistory historyItem)
         {
-            linkFlowContainer.AddText(prefix);
-
-            linkFlowContainer.AddText($@" {historyItem.Amount} kudosu", t =>
+            switch (historyItem.Action)
             {
-                t.Font = t.Font.With(italics: true);
-                t.Colour = colours.Blue;
-            });
+                case KudosuAction.VoteGive:
+                    return @"Received";
+
+                case KudosuAction.Give:
+                    return @"Received";
+
+                case KudosuAction.VoteReset:
+                    return @"Lost";
+
+                case KudosuAction.DenyKudosuReset:
+                    return @"Denied";
+
+                case KudosuAction.AllowKudosuGive:
+                    return @"Received";
+
+                case KudosuAction.DeleteReset:
+                    return @"Lost";
+
+                case KudosuAction.RestoreGive:
+                    return @"Received";
+
+                case KudosuAction.RecalculateGive:
+                    return @"Received";
+
+                case KudosuAction.RecalculateReset:
+                    return @"Lost";
+
+                default:
+                    return null;
+            }
         }
-
-        private void addMainPart(string text)
-        {
-            var formatted = MessageFormatter.FormatText(text);
-
-            linkFlowContainer.AddLinks(formatted.Text, formatted.Links);
-        }
-
-        private void addPostPart() => linkFlowContainer.AddLink(historyItem.Post.Title, historyItem.Post.Url);
     }
 }

From b03b520818eb2e9474fa7f7ea7daf01c287263b5 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 30 Aug 2019 10:13:21 +0300
Subject: [PATCH 22/29] Move Absing from the APIKudosuHistory

---
 osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs  | 6 ------
 .../Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs    | 3 ++-
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
index 19ce11aa13..67ff20e6c2 100644
--- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -13,12 +13,6 @@ namespace osu.Game.Online.API.Requests.Responses
         public DateTimeOffset CreatedAt;
 
         [JsonProperty("amount")]
-        private int amount
-        {
-            //We can receive negative values. However "action" is enough to build needed items
-            set => Amount = Math.Abs(value);
-        }
-
         public int Amount;
 
         [JsonProperty("post")]
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index fb7d597012..94733324ba 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -9,6 +9,7 @@ using osu.Game.Graphics.Containers;
 using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Online.API.Requests;
 using osu.Game.Online.Chat;
+using System;
 
 namespace osu.Game.Overlays.Profile.Sections.Kudosu
 {
@@ -56,7 +57,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             if (!string.IsNullOrEmpty(prefix))
             {
                 linkFlowContainer.AddText(prefix);
-                linkFlowContainer.AddText($@" {historyItem.Amount} kudosu", t =>
+                linkFlowContainer.AddText($@" {Math.Abs(historyItem.Amount)} kudosu", t =>
                 {
                     t.Font = t.Font.With(italics: true);
                     t.Colour = colours.Blue;

From 71c844facdff1219fc5b0a6e1189b81e6058c8d5 Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 30 Aug 2019 10:22:49 +0300
Subject: [PATCH 23/29] Remove unwanted spacings

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 24 ++++++++++---------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 94733324ba..408468fa73 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Online.API.Requests;
 using osu.Game.Online.Chat;
 using System;
+using osuTK;
 
 namespace osu.Game.Overlays.Profile.Sections.Kudosu
 {
@@ -37,6 +38,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                     Anchor = Anchor.CentreLeft,
                     Origin = Anchor.CentreLeft,
                     AutoSizeAxes = Axes.Both,
+                    Spacing = new Vector2(3),
                 },
                 date = new DrawableDate(historyItem.CreatedAt)
                 {
@@ -57,7 +59,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             if (!string.IsNullOrEmpty(prefix))
             {
                 linkFlowContainer.AddText(prefix);
-                linkFlowContainer.AddText($@" {Math.Abs(historyItem.Amount)} kudosu", t =>
+                linkFlowContainer.AddText($@"{Math.Abs(historyItem.Amount)} kudosu", t =>
                 {
                     t.Font = t.Font.With(italics: true);
                     t.Colour = colours.Blue;
@@ -75,40 +77,40 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             switch (historyItem.Action)
             {
                 case KudosuAction.VoteGive:
-                    return @" from obtaining votes in modding post of";
+                    return @"from obtaining votes in modding post of";
 
                 case KudosuAction.Give:
-                    return $@" from {userLink()} for a post at";
+                    return $@"from {userLink()} for a post at";
 
                 case KudosuAction.Reset:
                     return $@"Kudosu reset by {userLink()} for the post";
 
                 case KudosuAction.VoteReset:
-                    return @" from losing votes in modding post of";
+                    return @"from losing votes in modding post of";
 
                 case KudosuAction.DenyKudosuReset:
-                    return @" from modding post";
+                    return @"from modding post";
 
                 case KudosuAction.Revoke:
                     return $@"Denied kudosu by {userLink()} for the post";
 
                 case KudosuAction.AllowKudosuGive:
-                    return @" from kudosu deny repeal of modding post";
+                    return @"from kudosu deny repeal of modding post";
 
                 case KudosuAction.DeleteReset:
-                    return @" from modding post deletion of";
+                    return @"from modding post deletion of";
 
                 case KudosuAction.RestoreGive:
-                    return @" from modding post restoration of";
+                    return @"from modding post restoration of";
 
                 case KudosuAction.RecalculateGive:
-                    return @" from votes recalculation in modding post of";
+                    return @"from votes recalculation in modding post of";
 
                 case KudosuAction.RecalculateReset:
-                    return @" from votes recalculation in modding post of";
+                    return @"from votes recalculation in modding post of";
 
                 default:
-                    return @" from unknown event ";
+                    return @"from unknown event";
             }
         }
 

From 72dbeaec1632ce6a94c71bc82141d447a1f19d4f Mon Sep 17 00:00:00 2001
From: Andrei Zavatski <megaman9919@gmail.com>
Date: Fri, 30 Aug 2019 10:26:11 +0300
Subject: [PATCH 24/29] Fix the comment

---
 osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
index 67ff20e6c2..f2297f7a10 100644
--- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Online.API.Requests.Responses
         {
             set
             {
-                //We will receive something like "foo.bar" or just "foo"
+                //We will receive something like "event.action" or just "action"
                 string parsed = value.Contains(".") ? value.Split('.')[0].Pascalize() + value.Split('.')[1].Pascalize() : value.Pascalize();
 
                 Action = (KudosuAction)Enum.Parse(typeof(KudosuAction), parsed);

From c06908adf7c653f98fddda7d08f55e50a290b642 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 2 Sep 2019 15:37:38 +0900
Subject: [PATCH 25/29] Fix spacing specifications

---
 .../Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs   | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 408468fa73..9b81e8c573 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                     Anchor = Anchor.CentreLeft,
                     Origin = Anchor.CentreLeft,
                     AutoSizeAxes = Axes.Both,
-                    Spacing = new Vector2(3),
+                    Spacing = new Vector2(0, 3),
                 },
                 date = new DrawableDate(historyItem.CreatedAt)
                 {
@@ -59,14 +59,15 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             if (!string.IsNullOrEmpty(prefix))
             {
                 linkFlowContainer.AddText(prefix);
-                linkFlowContainer.AddText($@"{Math.Abs(historyItem.Amount)} kudosu", t =>
+                linkFlowContainer.AddText($@" {Math.Abs(historyItem.Amount)} kudosu ", t =>
                 {
                     t.Font = t.Font.With(italics: true);
                     t.Colour = colours.Blue;
                 });
             }
 
-            linkFlowContainer.AddLinks(formattedSource.Text + " ", formattedSource.Links);
+            linkFlowContainer.AddLinks(formattedSource.Text, formattedSource.Links);
+            linkFlowContainer.AddText(" ");
             linkFlowContainer.AddLink(historyItem.Post.Title, historyItem.Post.Url);
         }
 

From da4507037333adcda31a9512f101b667cddb77ee Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 2 Sep 2019 15:44:21 +0900
Subject: [PATCH 26/29] Group common prefixes together

---
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 20 ++++---------------
 1 file changed, 4 insertions(+), 16 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 9b81e8c573..ff64ea5648 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -120,29 +120,17 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             switch (historyItem.Action)
             {
                 case KudosuAction.VoteGive:
-                    return @"Received";
-
                 case KudosuAction.Give:
+                case KudosuAction.AllowKudosuGive:
+                case KudosuAction.RestoreGive:
+                case KudosuAction.RecalculateGive:
                     return @"Received";
 
-                case KudosuAction.VoteReset:
-                    return @"Lost";
-
                 case KudosuAction.DenyKudosuReset:
                     return @"Denied";
 
-                case KudosuAction.AllowKudosuGive:
-                    return @"Received";
-
                 case KudosuAction.DeleteReset:
-                    return @"Lost";
-
-                case KudosuAction.RestoreGive:
-                    return @"Received";
-
-                case KudosuAction.RecalculateGive:
-                    return @"Received";
-
+                case KudosuAction.VoteReset:
                 case KudosuAction.RecalculateReset:
                     return @"Lost";
 

From 3d551b08a96a1d9be3130a9b5bf27613fced44bd Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 2 Sep 2019 15:57:23 +0900
Subject: [PATCH 27/29] Rename legacy actions

---
 .../Visual/Online/TestSceneKudosuHistory.cs      |  6 +++---
 .../API/Requests/GetUserKudosuHistoryRequest.cs  | 16 ++++++++--------
 .../Sections/Kudosu/DrawableKudosuHistoryItem.cs |  8 ++++----
 3 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
index 8badfeaa23..a4f3bf65e6 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 5,
                 CreatedAt = new DateTimeOffset(new DateTime(2012, 10, 11)),
-                Action = KudosuAction.Give,
+                Action = KudosuAction.ForumGive,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 2",
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 8,
                 CreatedAt = new DateTimeOffset(new DateTime(2013, 9, 11)),
-                Action = KudosuAction.Reset,
+                Action = KudosuAction.ForumReset,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 3",
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 7,
                 CreatedAt = new DateTimeOffset(new DateTime(2014, 8, 11)),
-                Action = KudosuAction.Revoke,
+                Action = KudosuAction.ForumRevoke,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 4",
diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
index dd6f2ccf22..32aa0c15fa 100644
--- a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
@@ -21,16 +21,16 @@ namespace osu.Game.Online.API.Requests
 
     public enum KudosuAction
     {
-        Give,
-        VoteGive,
-        Reset,
-        VoteReset,
-        DenyKudosuReset,
-        Revoke,
         AllowKudosuGive,
         DeleteReset,
-        RestoreGive,
+        DenyKudosuReset,
+        ForumGive,
+        ForumReset,
+        ForumRevoke,
         RecalculateGive,
-        RecalculateReset
+        RecalculateReset,
+        RestoreGive,
+        VoteGive,
+        VoteReset,
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index ff64ea5648..5229269def 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -80,10 +80,10 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                 case KudosuAction.VoteGive:
                     return @"from obtaining votes in modding post of";
 
-                case KudosuAction.Give:
+                case KudosuAction.ForumGive:
                     return $@"from {userLink()} for a post at";
 
-                case KudosuAction.Reset:
+                case KudosuAction.ForumReset:
                     return $@"Kudosu reset by {userLink()} for the post";
 
                 case KudosuAction.VoteReset:
@@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                 case KudosuAction.DenyKudosuReset:
                     return @"from modding post";
 
-                case KudosuAction.Revoke:
+                case KudosuAction.ForumRevoke:
                     return $@"Denied kudosu by {userLink()} for the post";
 
                 case KudosuAction.AllowKudosuGive:
@@ -120,7 +120,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             switch (historyItem.Action)
             {
                 case KudosuAction.VoteGive:
-                case KudosuAction.Give:
+                case KudosuAction.ForumGive:
                 case KudosuAction.AllowKudosuGive:
                 case KudosuAction.RestoreGive:
                 case KudosuAction.RecalculateGive:

From 0985b1679f86de6a4a96d34edb0e84cb30a55dac Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 2 Sep 2019 15:57:55 +0900
Subject: [PATCH 28/29] Move enum to response class

---
 .../Visual/Online/TestSceneKudosuHistory.cs       |  1 -
 .../API/Requests/GetUserKudosuHistoryRequest.cs   | 15 ---------------
 .../API/Requests/Responses/APIKudosuHistory.cs    | 15 +++++++++++++++
 .../Sections/Kudosu/DrawableKudosuHistoryItem.cs  |  1 -
 4 files changed, 15 insertions(+), 17 deletions(-)

diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
index a4f3bf65e6..84152e40fa 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -10,7 +10,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics;
 using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.API.Requests;
 using osu.Framework.Extensions.IEnumerableExtensions;
 
 namespace osu.Game.Tests.Visual.Online
diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
index 32aa0c15fa..e90e297672 100644
--- a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
@@ -18,19 +18,4 @@ namespace osu.Game.Online.API.Requests
 
         protected override string Target => $"users/{userId}/kudosu";
     }
-
-    public enum KudosuAction
-    {
-        AllowKudosuGive,
-        DeleteReset,
-        DenyKudosuReset,
-        ForumGive,
-        ForumReset,
-        ForumRevoke,
-        RecalculateGive,
-        RecalculateReset,
-        RestoreGive,
-        VoteGive,
-        VoteReset,
-    }
 }
diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
index f2297f7a10..25b11a6cf9 100644
--- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -53,4 +53,19 @@ namespace osu.Game.Online.API.Requests.Responses
 
         public KudosuAction Action;
     }
+
+    public enum KudosuAction
+    {
+        AllowKudosuGive,
+        DeleteReset,
+        DenyKudosuReset,
+        ForumGive,
+        ForumReset,
+        ForumRevoke,
+        RecalculateGive,
+        RecalculateReset,
+        RestoreGive,
+        VoteGive,
+        VoteReset,
+    }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 5229269def..6efe9825ec 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.API.Requests;
 using osu.Game.Online.Chat;
 using System;
 using osuTK;

From d21d68b36c4271d611db888c1917e09ee93b95ba Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 2 Sep 2019 16:34:11 +0900
Subject: [PATCH 29/29] Refactor to match web implementation 1:1

---
 .../Visual/Online/TestSceneKudosuHistory.cs   |  33 +++--
 .../Requests/Responses/APIKudosuHistory.cs    |  44 ++++--
 .../Kudosu/DrawableKudosuHistoryItem.cs       | 138 +++++++++---------
 3 files changed, 122 insertions(+), 93 deletions(-)

diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
index 84152e40fa..325d657f0e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -58,7 +58,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 10,
                 CreatedAt = new DateTimeOffset(new DateTime(2011, 11, 11)),
-                Action = KudosuAction.DenyKudosuReset,
+                Source = KudosuSource.DenyKudosu,
+                Action = KudosuAction.Reset,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 1",
@@ -74,7 +75,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 5,
                 CreatedAt = new DateTimeOffset(new DateTime(2012, 10, 11)),
-                Action = KudosuAction.ForumGive,
+                Source = KudosuSource.Forum,
+                Action = KudosuAction.Give,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 2",
@@ -90,7 +92,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 8,
                 CreatedAt = new DateTimeOffset(new DateTime(2013, 9, 11)),
-                Action = KudosuAction.ForumReset,
+                Source = KudosuSource.Forum,
+                Action = KudosuAction.Reset,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 3",
@@ -106,7 +109,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 7,
                 CreatedAt = new DateTimeOffset(new DateTime(2014, 8, 11)),
-                Action = KudosuAction.ForumRevoke,
+                Source = KudosuSource.Forum,
+                Action = KudosuAction.Revoke,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 4",
@@ -122,7 +126,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 100,
                 CreatedAt = new DateTimeOffset(new DateTime(2015, 7, 11)),
-                Action = KudosuAction.VoteGive,
+                Source = KudosuSource.Vote,
+                Action = KudosuAction.Give,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 5",
@@ -138,7 +143,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 20,
                 CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
-                Action = KudosuAction.VoteReset,
+                Source = KudosuSource.Vote,
+                Action = KudosuAction.Reset,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 6",
@@ -154,7 +160,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 11,
                 CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
-                Action = KudosuAction.AllowKudosuGive,
+                Source = KudosuSource.AllowKudosu,
+                Action = KudosuAction.Give,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 7",
@@ -170,7 +177,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 24,
                 CreatedAt = new DateTimeOffset(new DateTime(2014, 6, 11)),
-                Action = KudosuAction.DeleteReset,
+                Source = KudosuSource.Delete,
+                Action = KudosuAction.Reset,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 8",
@@ -186,7 +194,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 12,
                 CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
-                Action = KudosuAction.RestoreGive,
+                Source = KudosuSource.Restore,
+                Action = KudosuAction.Give,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 9",
@@ -202,7 +211,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 2,
                 CreatedAt = new DateTimeOffset(new DateTime(2012, 6, 11)),
-                Action = KudosuAction.RecalculateGive,
+                Source = KudosuSource.Recalculate,
+                Action = KudosuAction.Give,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 10",
@@ -218,7 +228,8 @@ namespace osu.Game.Tests.Visual.Online
             {
                 Amount = 32,
                 CreatedAt = new DateTimeOffset(new DateTime(2019, 8, 11)),
-                Action = KudosuAction.RecalculateReset,
+                Source = KudosuSource.Recalculate,
+                Action = KudosuAction.Reset,
                 Post = new APIKudosuHistory.ModdingPost
                 {
                     Title = @"Random post 11",
diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
index 25b11a6cf9..d596ddc560 100644
--- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -2,7 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using Humanizer;
+using System.Linq;
 using Newtonsoft.Json;
 
 namespace osu.Game.Online.API.Requests.Responses
@@ -39,33 +39,45 @@ namespace osu.Game.Online.API.Requests.Responses
             public string Username;
         }
 
+        public KudosuSource Source;
+
+        public KudosuAction Action;
+
         [JsonProperty("action")]
         private string action
         {
             set
             {
-                //We will receive something like "event.action" or just "action"
-                string parsed = value.Contains(".") ? value.Split('.')[0].Pascalize() + value.Split('.')[1].Pascalize() : value.Pascalize();
+                // incoming action may contain a prefix. if it doesn't, it's a legacy forum event.
 
-                Action = (KudosuAction)Enum.Parse(typeof(KudosuAction), parsed);
+                string[] split = value.Split('.');
+
+                if (split.Length > 1)
+                    Enum.TryParse(split.First().Replace("_", ""), true, out Source);
+                else
+                    Source = KudosuSource.Forum;
+
+                Enum.TryParse(split.Last(), true, out Action);
             }
         }
+    }
 
-        public KudosuAction Action;
+    public enum KudosuSource
+    {
+        Unknown,
+        AllowKudosu,
+        Delete,
+        DenyKudosu,
+        Forum,
+        Recalculate,
+        Restore,
+        Vote
     }
 
     public enum KudosuAction
     {
-        AllowKudosuGive,
-        DeleteReset,
-        DenyKudosuReset,
-        ForumGive,
-        ForumReset,
-        ForumRevoke,
-        RecalculateGive,
-        RecalculateReset,
-        RestoreGive,
-        VoteGive,
-        VoteReset,
+        Give,
+        Reset,
+        Revoke,
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
index 6efe9825ec..d0cfe9fa54 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -51,91 +51,97 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
         private void load()
         {
             date.Colour = colours.GreySeafoamLighter;
-
-            string prefix = getPrefix(historyItem);
-            var formattedSource = MessageFormatter.FormatText(getSource(historyItem));
-
-            if (!string.IsNullOrEmpty(prefix))
-            {
-                linkFlowContainer.AddText(prefix);
-                linkFlowContainer.AddText($@" {Math.Abs(historyItem.Amount)} kudosu ", t =>
-                {
-                    t.Font = t.Font.With(italics: true);
-                    t.Colour = colours.Blue;
-                });
-            }
-
+            var formattedSource = MessageFormatter.FormatText(getString(historyItem));
             linkFlowContainer.AddLinks(formattedSource.Text, formattedSource.Links);
-            linkFlowContainer.AddText(" ");
-            linkFlowContainer.AddLink(historyItem.Post.Title, historyItem.Post.Url);
         }
 
-        private string getSource(APIKudosuHistory historyItem)
+        private string getString(APIKudosuHistory item)
         {
-            string userLink() => $"[{historyItem.Giver?.Url} {historyItem.Giver?.Username}]";
+            string amount = $"{Math.Abs(item.Amount)} kudosu";
+            string post = $"[{item.Post.Title}]({item.Post.Url})";
 
-            switch (historyItem.Action)
+            switch (item.Source)
             {
-                case KudosuAction.VoteGive:
-                    return @"from obtaining votes in modding post of";
+                case KudosuSource.AllowKudosu:
+                    switch (item.Action)
+                    {
+                        case KudosuAction.Give:
+                            return $"Received {amount} from kudosu deny repeal of modding post {post}";
+                    }
 
-                case KudosuAction.ForumGive:
-                    return $@"from {userLink()} for a post at";
+                    break;
 
-                case KudosuAction.ForumReset:
-                    return $@"Kudosu reset by {userLink()} for the post";
+                case KudosuSource.DenyKudosu:
+                    switch (item.Action)
+                    {
+                        case KudosuAction.Reset:
+                            return $"Denied {amount} from modding post {post}";
+                    }
 
-                case KudosuAction.VoteReset:
-                    return @"from losing votes in modding post of";
+                    break;
 
-                case KudosuAction.DenyKudosuReset:
-                    return @"from modding post";
+                case KudosuSource.Delete:
+                    switch (item.Action)
+                    {
+                        case KudosuAction.Reset:
+                            return $"Lost {amount} from modding post deletion of {post}";
+                    }
 
-                case KudosuAction.ForumRevoke:
-                    return $@"Denied kudosu by {userLink()} for the post";
+                    break;
 
-                case KudosuAction.AllowKudosuGive:
-                    return @"from kudosu deny repeal of modding post";
+                case KudosuSource.Restore:
+                    switch (item.Action)
+                    {
+                        case KudosuAction.Give:
+                            return $"Received {amount} from modding post restoration of {post}";
+                    }
 
-                case KudosuAction.DeleteReset:
-                    return @"from modding post deletion of";
+                    break;
 
-                case KudosuAction.RestoreGive:
-                    return @"from modding post restoration of";
+                case KudosuSource.Vote:
+                    switch (item.Action)
+                    {
+                        case KudosuAction.Give:
+                            return $"Received {amount} from obtaining votes in modding post of {post}";
 
-                case KudosuAction.RecalculateGive:
-                    return @"from votes recalculation in modding post of";
+                        case KudosuAction.Reset:
+                            return $"Lost {amount} from losing votes in modding post of {post}";
+                    }
 
-                case KudosuAction.RecalculateReset:
-                    return @"from votes recalculation in modding post of";
+                    break;
 
-                default:
-                    return @"from unknown event";
+                case KudosuSource.Recalculate:
+                    switch (item.Action)
+                    {
+                        case KudosuAction.Give:
+                            return $"Received {amount} from votes recalculation in modding post of {post}";
+
+                        case KudosuAction.Reset:
+                            return $"Lost {amount} from votes recalculation in modding post of {post}";
+                    }
+
+                    break;
+
+                case KudosuSource.Forum:
+
+                    string giver = $"[{item.Giver?.Username}]({item.Giver?.Url})";
+
+                    switch (historyItem.Action)
+                    {
+                        case KudosuAction.Give:
+                            return $"Received {amount} from {giver} for a post at {post}";
+
+                        case KudosuAction.Reset:
+                            return $"Kudosu reset by {giver} for the post {post}";
+
+                        case KudosuAction.Revoke:
+                            return $"Denied kudosu by {giver} for the post {post}";
+                    }
+
+                    break;
             }
-        }
 
-        private string getPrefix(APIKudosuHistory historyItem)
-        {
-            switch (historyItem.Action)
-            {
-                case KudosuAction.VoteGive:
-                case KudosuAction.ForumGive:
-                case KudosuAction.AllowKudosuGive:
-                case KudosuAction.RestoreGive:
-                case KudosuAction.RecalculateGive:
-                    return @"Received";
-
-                case KudosuAction.DenyKudosuReset:
-                    return @"Denied";
-
-                case KudosuAction.DeleteReset:
-                case KudosuAction.VoteReset:
-                case KudosuAction.RecalculateReset:
-                    return @"Lost";
-
-                default:
-                    return null;
-            }
+            return $"Unknown event ({amount} change)";
         }
     }
 }