1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-30 09:22:54 +08:00
osu-lazer/osu.Game/Overlays/Comments/CommentsContainer.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

439 lines
17 KiB
C#
Raw Normal View History

2019-10-07 22:49:20 +08:00
// 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.
2022-06-17 15:37:17 +08:00
#nullable disable
2023-01-07 09:15:43 +08:00
using System;
2019-10-07 22:49:20 +08:00
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
2020-02-26 23:00:48 +08:00
using System.Threading;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Threading;
2020-02-26 22:38:50 +08:00
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Localisation;
2023-01-18 04:30:46 +08:00
using osu.Framework.Logging;
2020-02-26 22:38:50 +08:00
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users.Drawables;
using osuTK;
2019-10-07 22:49:20 +08:00
namespace osu.Game.Overlays.Comments
2019-10-07 22:49:20 +08:00
{
[Cached]
2019-10-07 22:49:20 +08:00
public partial class CommentsContainer : CompositeDrawable
{
private readonly Bindable<CommentableType> type = new Bindable<CommentableType>();
private readonly BindableLong id = new BindableLong();
public IBindable<CommentableType> Type => type;
public IBindable<long> Id => id;
2019-10-07 22:49:20 +08:00
2019-10-13 16:23:49 +08:00
public readonly Bindable<CommentsSortCriteria> Sort = new Bindable<CommentsSortCriteria>();
2019-10-09 17:18:49 +08:00
public readonly BindableBool ShowDeleted = new BindableBool();
2019-10-07 22:49:20 +08:00
protected readonly IBindable<APIUser> User = new Bindable<APIUser>();
2019-10-07 22:49:20 +08:00
[Resolved]
private IAPIProvider api { get; set; }
private GetCommentsRequest request;
private ScheduledDelegate scheduledCommentsLoad;
2020-02-26 23:00:48 +08:00
private CancellationTokenSource loadCancellation;
2019-10-13 19:43:30 +08:00
private int currentPage;
2021-08-13 20:01:52 +08:00
private FillFlowContainer pinnedContent;
private FillFlowContainer content;
private DeletedCommentsCounter deletedCommentsCounter;
private CommentsShowMoreButton moreButton;
2021-08-13 12:24:05 +08:00
private TotalCommentsCounter commentCounter;
private UpdateableAvatar avatar;
2019-10-07 22:49:20 +08:00
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
2019-10-07 22:49:20 +08:00
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
2019-10-07 22:49:20 +08:00
AddRangeInternal(new Drawable[]
{
new Box
2019-10-07 22:49:20 +08:00
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5
2019-10-07 22:49:20 +08:00
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
2021-08-13 12:24:05 +08:00
commentCounter = new TotalCommentsCounter(),
2021-08-13 20:01:52 +08:00
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
pinnedContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
},
},
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 },
Children = new Drawable[]
{
avatar = new UpdateableAvatar(api.LocalUser.Value, isInteractive: false)
{
Size = new Vector2(50),
CornerExponent = 2,
CornerRadius = 25,
Masking = true,
},
new Container
{
Padding = new MarginPadding { Left = 60 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
2023-01-07 09:15:43 +08:00
Child = new NewCommentEditor
{
OnPost = prependPostedComments
}
}
}
},
2019-10-07 23:45:22 +08:00
new CommentsHeader
{
2019-10-09 17:18:49 +08:00
Sort = { BindTarget = Sort },
ShowDeleted = { BindTarget = ShowDeleted }
},
content = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
2019-10-13 17:38:50 +08:00
},
new Container
{
Name = @"Footer",
2019-10-13 17:38:50 +08:00
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
2019-10-13 19:43:30 +08:00
new FillFlowContainer
2019-10-13 17:38:50 +08:00
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Bottom = 20 },
2019-10-13 17:38:50 +08:00
Children = new Drawable[]
{
deletedCommentsCounter = new DeletedCommentsCounter
2019-10-13 17:38:50 +08:00
{
ShowDeleted = { BindTarget = ShowDeleted },
Margin = new MarginPadding
{
Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING,
Vertical = 10
}
2019-10-13 19:43:30 +08:00
},
new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Child = moreButton = new CommentsShowMoreButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding
{
Vertical = 10
},
Action = getComments,
IsLoading = true,
2019-10-13 19:43:30 +08:00
}
2019-10-13 17:38:50 +08:00
}
}
}
}
}
}
}
});
User.BindTo(api.LocalUser);
2019-10-15 16:26:58 +08:00
}
protected override void LoadComplete()
{
User.BindValueChanged(_ => refetchComments());
User.BindValueChanged(e => avatar.User = e.NewValue);
Sort.BindValueChanged(_ => refetchComments(), true);
base.LoadComplete();
}
/// <param name="type">The type of resource to get comments for.</param>
/// <param name="id">The id of the resource to get comments for.</param>
public void ShowComments(CommentableType type, long id)
{
this.type.Value = type;
this.id.Value = id;
if (!IsLoaded)
return;
// only reset when changing ID/type. other refetch ops are generally just changing sort order.
2021-08-13 12:24:05 +08:00
commentCounter.Current.Value = 0;
refetchComments();
}
2020-01-07 17:30:06 +08:00
private void refetchComments()
{
ClearComments();
2019-10-15 16:25:58 +08:00
getComments();
}
2019-10-13 19:43:30 +08:00
2019-10-15 16:25:58 +08:00
private void getComments()
{
2020-02-21 18:50:16 +08:00
if (id.Value <= 0)
2020-01-07 17:29:21 +08:00
return;
request?.Cancel();
2020-02-26 23:00:48 +08:00
loadCancellation?.Cancel();
scheduledCommentsLoad?.Cancel();
2020-02-21 17:42:11 +08:00
request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0);
2020-07-12 07:20:54 +08:00
request.Success += res => scheduledCommentsLoad = Schedule(() => OnSuccess(res));
2020-02-05 00:15:23 +08:00
api.PerformAsync(request);
}
protected void ClearComments()
2019-10-15 16:25:58 +08:00
{
currentPage = 1;
deletedCommentsCounter.Count.Value = 0;
moreButton.Show();
2019-10-15 16:25:58 +08:00
moreButton.IsLoading = true;
2021-08-13 20:01:52 +08:00
pinnedContent.Clear();
2019-10-15 16:25:58 +08:00
content.Clear();
2020-02-27 06:38:21 +08:00
CommentDictionary.Clear();
2019-10-15 16:25:58 +08:00
}
2020-02-27 06:38:21 +08:00
protected readonly Dictionary<long, DrawableComment> CommentDictionary = new Dictionary<long, DrawableComment>();
2020-02-26 22:38:50 +08:00
protected void OnSuccess(CommentBundle response)
{
2021-08-13 12:24:05 +08:00
commentCounter.Current.Value = response.Total;
2020-02-26 22:38:50 +08:00
if (!response.Comments.Any())
{
2020-02-26 22:38:50 +08:00
content.Add(new NoCommentsPlaceholder());
2020-02-26 23:00:48 +08:00
moreButton.Hide();
2020-02-26 22:38:50 +08:00
return;
}
2019-10-13 17:10:01 +08:00
AppendComments(response);
2020-02-26 22:38:50 +08:00
}
/// <summary>
/// Appends retrieved comments to the subtree rooted of comments in this page.
/// </summary>
/// <param name="bundle">The bundle of comments to add.</param>
protected void AppendComments([NotNull] CommentBundle bundle)
2020-02-26 22:38:50 +08:00
{
2020-02-26 23:00:48 +08:00
var topLevelComments = new List<DrawableComment>();
2020-02-26 22:38:50 +08:00
var orphaned = new List<Comment>();
2021-08-13 20:01:52 +08:00
foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments).Concat(bundle.PinnedComments))
2020-02-26 22:38:50 +08:00
{
// Exclude possible duplicated comments.
2020-02-27 06:38:21 +08:00
if (CommentDictionary.ContainsKey(comment.Id))
2020-02-26 22:38:50 +08:00
continue;
addNewComment(comment);
}
// Comments whose parents were seen later than themselves can now be added.
foreach (var o in orphaned)
addNewComment(o);
if (topLevelComments.Any())
{
LoadComponentsAsync(topLevelComments, loaded =>
{
2021-08-13 20:01:52 +08:00
pinnedContent.AddRange(loaded.Where(d => d.Comment.Pinned));
content.AddRange(loaded.Where(d => !d.Comment.Pinned));
deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel);
if (bundle.HasMore)
{
int loadedTopLevelComments = 0;
2022-06-24 20:25:23 +08:00
pinnedContent.Children.OfType<DrawableComment>().ForEach(_ => loadedTopLevelComments++);
content.Children.OfType<DrawableComment>().ForEach(_ => loadedTopLevelComments++);
moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments;
moreButton.IsLoading = false;
}
else
{
moreButton.Hide();
}
}, (loadCancellation = new CancellationTokenSource()).Token);
}
2020-02-26 23:00:48 +08:00
2020-02-26 22:38:50 +08:00
void addNewComment(Comment comment)
{
var drawableComment = GetDrawableComment(comment, bundle.CommentableMeta);
2020-02-26 22:38:50 +08:00
if (comment.ParentId == null)
{
// Comments that have no parent are added as top-level comments to the flow.
2020-02-26 23:00:48 +08:00
topLevelComments.Add(drawableComment);
2020-02-26 22:38:50 +08:00
}
2020-02-27 06:38:21 +08:00
else if (CommentDictionary.TryGetValue(comment.ParentId.Value, out var parentDrawable))
2020-02-26 22:38:50 +08:00
{
// The comment's parent has already been seen, so the parent<-> child links can be added.
comment.ParentComment = parentDrawable.Comment;
parentDrawable.Replies.Add(drawableComment);
2019-10-13 19:43:30 +08:00
}
2020-01-31 14:46:35 +08:00
else
{
2020-02-26 22:38:50 +08:00
// The comment's parent has not been seen yet, so keep it orphaned for the time being. This can occur if the comments arrive out of order.
// Since this comment has now been seen, any further children can be added to it without being orphaned themselves.
orphaned.Add(comment);
2020-01-31 14:46:35 +08:00
}
2020-02-26 22:38:50 +08:00
}
}
2023-01-07 09:15:43 +08:00
private void prependPostedComments(CommentBundle bundle)
{
var topLevelComments = new List<DrawableComment>();
foreach (var comment in bundle.Comments)
{
// Exclude possible duplicated comments.
if (CommentDictionary.ContainsKey(comment.Id))
continue;
topLevelComments.Add(GetDrawableComment(comment, bundle.CommentableMeta));
2023-01-07 09:15:43 +08:00
}
if (topLevelComments.Any())
{
LoadComponentsAsync(topLevelComments, loaded =>
{
if (content.Count > 0 && content[0] is NoCommentsPlaceholder placeholder)
2023-01-07 09:15:43 +08:00
content.Remove(placeholder, true);
foreach (var comment in loaded)
{
content.Insert((int)-Clock.CurrentTime, comment);
2023-01-07 09:15:43 +08:00
}
}, (loadCancellation = new CancellationTokenSource()).Token);
}
}
public DrawableComment GetDrawableComment(Comment comment, IReadOnlyList<CommentableMeta> meta)
2020-02-26 22:38:50 +08:00
{
2020-02-27 06:38:21 +08:00
if (CommentDictionary.TryGetValue(comment.Id, out var existing))
2020-02-26 22:38:50 +08:00
return existing;
return CommentDictionary[comment.Id] = new DrawableComment(comment, meta)
2020-02-26 22:38:50 +08:00
{
ShowDeleted = { BindTarget = ShowDeleted },
Sort = { BindTarget = Sort },
RepliesRequested = onCommentRepliesRequested
};
}
private void onCommentRepliesRequested(DrawableComment drawableComment, int page)
{
2020-02-26 23:52:58 +08:00
var req = new GetCommentsRequest(id.Value, type.Value, Sort.Value, page, drawableComment.Comment.Id);
2019-10-14 22:33:14 +08:00
2020-02-27 06:38:21 +08:00
req.Success += response => Schedule(() => AppendComments(response));
2020-02-26 22:38:50 +08:00
2020-02-26 23:52:58 +08:00
api.PerformAsync(req);
2019-10-07 22:49:20 +08:00
}
2019-10-13 21:22:10 +08:00
protected override void Dispose(bool isDisposing)
{
request?.Cancel();
2020-02-26 23:00:48 +08:00
loadCancellation?.Cancel();
2019-10-13 21:22:10 +08:00
base.Dispose(isDisposing);
}
2020-02-26 22:38:50 +08:00
internal partial class NoCommentsPlaceholder : CompositeDrawable
2020-02-26 22:38:50 +08:00
{
[BackgroundDependencyLoader]
2022-01-15 08:06:39 +08:00
private void load()
2020-02-26 22:38:50 +08:00
{
Height = 80;
RelativeSizeAxes = Axes.X;
AddRangeInternal(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING },
Text = CommentsStrings.Empty
2020-02-26 22:38:50 +08:00
}
});
}
}
private partial class NewCommentEditor : CommentEditor
{
[Resolved]
private CommentsContainer commentsContainer { get; set; }
2023-01-07 09:15:43 +08:00
public Action<CommentBundle> OnPost;
//TODO should match web, left empty due to no multiline support
protected override LocalisableString FooterText => default;
2023-06-23 05:00:52 +08:00
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew;
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin;
protected override void OnCommit(string text)
{
2023-01-14 07:41:11 +08:00
ShowLoadingSpinner = true;
CommentPostRequest req = new CommentPostRequest(commentsContainer.Type.Value, commentsContainer.Id.Value, text);
2023-01-18 04:30:46 +08:00
req.Failure += e => Schedule(() =>
{
2023-01-14 07:41:11 +08:00
ShowLoadingSpinner = false;
2023-01-18 04:30:46 +08:00
Logger.Error(e, "Posting comment failed.");
});
req.Success += cb => Schedule(() =>
{
2023-01-14 07:41:11 +08:00
ShowLoadingSpinner = false;
Current.Value = string.Empty;
2023-01-07 09:15:43 +08:00
OnPost?.Invoke(cb);
});
API.Queue(req);
}
}
2019-10-07 22:49:20 +08:00
}
}