#495 comments pagination

This commit is contained in:
Hamid Reza Mohammadi 2026-08-04 19:00:08 +03:30
parent 4007f93cc4
commit 1e4527591c
13 changed files with 230 additions and 29 deletions

View File

@ -34,6 +34,8 @@
btshmr(@Model.GanjoorPage.Id);
}
checkIfBookmarked(@Model.GanjoorPage.Id);
paginateComments();
revealCommentFromUrlHash();
});
</script>
}

View File

@ -130,6 +130,67 @@ namespace GanjooRazor.Pages
};
}
/// <summary>
/// re-fetch a poem's comments sorted by rating or by date (AJAX, called from sortComments() in bk.js)
/// </summary>
public async Task<IActionResult> OnGetCommentsPartialAsync(int poemId, bool sortByRanking = true)
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poem/{poemId}/comments?sortByRanking={sortByRanking}");
if (!response.IsSuccessStatusCode)
{
return BadRequest(await ReadErrorMessageAsync(response));
}
var comments = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<GanjoorCommentSummaryViewModel[]>();
LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]);
if (!string.IsNullOrEmpty(Request.Cookies["UserId"])
&& Guid.TryParse(Request.Cookies["UserId"], out Guid userId)
&& userId != Guid.Empty)
{
using (HttpClient secureClient = new HttpClient())
{
if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
{
GanjoorUserBookmarkViewModel[] bookmarks = Array.Empty<GanjoorUserBookmarkViewModel>();
HttpResponseMessage bookmarksResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/ganjoor/bookmark/{poemId}");
if (bookmarksResponse.IsSuccessStatusCode)
{
bookmarks = JsonConvert.DeserializeObject<GanjoorUserBookmarkViewModel[]>(await bookmarksResponse.Content.ReadAsStringAsync());
}
foreach (GanjoorCommentSummaryViewModel comment in comments)
{
comment.MyComment = comment.UserId == userId;
comment.IsBookmarked = bookmarks.Where(b => b.CoupletIndex == -comment.Id).Any();
_markMyReplies(comment, userId, bookmarks);
}
HttpResponseMessage ratingsResponse = await secureClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poem/{poemId}/comments/myratings");
if (ratingsResponse.IsSuccessStatusCode)
{
var ratings = JsonConvert.DeserializeObject<GanjoorCommentUserRatingViewModel[]>(await ratingsResponse.Content.ReadAsStringAsync());
if (ratings.Length > 0)
{
var ratingsDic = ratings.ToDictionary(r => r.CommentId, r => r.Value);
foreach (GanjoorCommentSummaryViewModel comment in comments)
{
_applyCommentRatings(comment, ratingsDic);
}
}
}
}
}
}
return Partial("~/Pages/Partials/GanjoorPage/_CommentsListPartial.cshtml", new _CommentsListPartialModel()
{
Comments = comments,
PoemId = poemId,
LoggedIn = LoggedIn,
});
}
public Task<IActionResult> OnPostReply(string replyCommentText, int refPoemId, int refCommentId)
{
return OnPostComment(replyCommentText, refPoemId, refCommentId, -1);

View File

@ -23,7 +23,7 @@
font-family: 'Vazirmatn'
}
</style>
<script src="/js/bk.js?version=83"></script>
<script src="/js/bk.js?version=86"></script>
<script src="~/js/jquery-1.10.2.min.js"></script>
<script src="~/js/leaflet.js"></script>
<script src="~/js/leaflet-providers.js"></script>

View File

@ -15,7 +15,7 @@
<link rel="shortcut icon" href="/favicon.ico?version=1" type="image/x-icon" />
<link rel="search" type="application/opensearchdescription+xml" title="گنجور" href="https://ganjoor.net/search.xml" />
<script src="/lib/jquery/dist/jquery.min.js"></script>
<script src="/js/bk.js?version=83"></script>
<script src="/js/bk.js?version=86"></script>
<script>
var cookieValue = getCookie("playerScrollLock");
var playerScrollLock = cookieValue === "true";

View File

@ -872,30 +872,16 @@
</div>
<div class="related-images-frame poemtabcontent">
<div id="comments-section">
@switch (Model.GanjoorPage.Poem.Comments.Length)
@if (Model.GanjoorPage.Poem.Comments.Length > 1)
{
case 0:
<p>
تا به حال حاشیه‌ای برای این شعر نوشته نشده است. <a href="#commentform" class="comments-link" title="من حاشیه بگذارم ...">💬 من حاشیه بگذارم ...</a>
</p>
break;
case 1:
<p>
تا به حال یک حاشیه برای این شعر نوشته شده است. <a href="#commentform" class="comments-link" title="من حاشیه بگذارم ...">💬 من حاشیه بگذارم ...</a>
</p>
break;
default:
<p>
تا به حال @Html.Raw(Model.GanjoorPage.Poem.Comments.Length.ToPersianNumbers()) حاشیه برای این شعر نوشته شده است. <a href="#commentform" class="comments-link" title="من حاشیه بگذارم ...">💬 من حاشیه بگذارم ...</a>
</p>
break;
<p class="comments-sort-toggle">
مرتب‌سازی حاشیه‌ها:
<a role="button" class="cursor-pointer" onclick="sortComments(@Model.GanjoorPage.Id, true)">محبوب‌ترین</a> |
<a role="button" class="cursor-pointer" onclick="sortComments(@Model.GanjoorPage.Id, false)">تازه‌ترین</a>
</p>
}
<div id="comments-block">
@foreach (var comment in Model.GanjoorPage.Poem.Comments)
{
<partial name="~/Pages/Partials/GanjoorPage/_CommentPartial.cshtml" model=@Model.GetCommentModel(comment, Model.GanjoorPage.Id) />
}
<div id="comments-list-wrapper">
<partial name="~/Pages/Partials/GanjoorPage/_CommentsListPartial.cshtml" model=@(new GanjooRazor.Pages._CommentsListPartialModel() { Comments = Model.GanjoorPage.Poem.Comments, PoemId = Model.GanjoorPage.Id, LoggedIn = Model.LoggedIn }) />
</div>
@if (Model.LoggedIn)

View File

@ -0,0 +1,31 @@
@model GanjooRazor.Pages._CommentsListPartialModel
@using DNTPersianUtils.Core
@{
Layout = null;
}
@switch (Model.Comments.Length)
{
case 0:
<p>
تا به حال حاشیه‌ای برای این شعر نوشته نشده است. <a href="#commentform" class="comments-link" title="من حاشیه بگذارم ...">💬 من حاشیه بگذارم ...</a>
</p>
break;
case 1:
<p>
تا به حال یک حاشیه برای این شعر نوشته شده است. <a href="#commentform" class="comments-link" title="من حاشیه بگذارم ...">💬 من حاشیه بگذارم ...</a>
</p>
break;
default:
<p>
تا به حال @Html.Raw(Model.Comments.Length.ToPersianNumbers()) حاشیه برای این شعر نوشته شده است. <a href="#commentform" class="comments-link" title="من حاشیه بگذارم ...">💬 من حاشیه بگذارم ...</a>
</p>
break;
}
<div id="comments-block">
@foreach (var comment in Model.Comments)
{
<partial name="~/Pages/Partials/GanjoorPage/_CommentPartial.cshtml" model=Model.GetCommentModel(comment) />
}
</div>

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using RMuseum.Models.Ganjoor.ViewModels;
namespace GanjooRazor.Pages
{
/// <summary>
/// Model for the comments list + count message shared between the initial page render
/// (_PoemPagePartial.cshtml) and the AJAX sort-toggle refresh (OnGetCommentsPartialAsync).
/// </summary>
public class _CommentsListPartialModel : PageModel
{
public GanjoorCommentSummaryViewModel[] Comments { get; set; }
public int PoemId { get; set; }
public bool LoggedIn { get; set; }
public _CommentPartialModel GetCommentModel(GanjoorCommentSummaryViewModel comment)
{
return new _CommentPartialModel()
{
Comment = comment,
Error = "",
InReplyTo = null,
LoggedIn = LoggedIn,
DivSuffix = "",
PoemId = PoemId,
};
}
}
}

View File

@ -150,7 +150,7 @@
</p>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/js/bk.js?version=83"></script>
<script src="~/js/bk.js?version=86"></script>
<script src="~/js/jplayersupport.js"></script>
<script src="~/js/vaabd.js?version=10"></script>
<script src="~/dist/jplayer/jquery.jplayer.min.js"></script>

View File

@ -238,7 +238,7 @@
<script src="~/js/bk.js?version=83"></script>
<script src="~/js/bk.js?version=86"></script>
<script src="~/js/jplayersupport.js"></script>
<script src="~/js/vaabd.js?version=10"></script>
<script src="~/dist/jplayer/jquery.jplayer.min.js"></script>

View File

@ -35,7 +35,7 @@
});
</script>
<script src="/js/bk.js?version=83"></script>
<script src="/js/bk.js?version=86"></script>
<script>
function editBanner(bannerId) {
var url = '?handler=Edit';

View File

@ -34,7 +34,7 @@
<link rel="shortcut icon" href="/favicon.ico?version=1" type="image/x-icon" />
<link rel="search" type="application/opensearchdescription+xml" title="گنجور" href="https://ganjoor.net/search.xml" />
<script src="/lib/jquery/dist/jquery.min.js"></script>
<script src="/js/bk.js?version=83"></script>
<script src="/js/bk.js?version=86"></script>
<script>
var cookieValue = getCookie("playerScrollLock");
var playerScrollLock = cookieValue === "true";

View File

@ -32,7 +32,7 @@
});
</script>
<script src="/lib/diff.js"></script>
<script src="/js/bk.js?version=83"></script>
<script src="/js/bk.js?version=86"></script>
<script>
function compareCorrectionBlocks(originalElemenyId, editedElementId, resultElementId) {
var a = document.getElementById(originalElemenyId);

View File

@ -504,6 +504,98 @@ function switchBookmarkInternal(poemId, coupletIndex, divSuffix) {
});
}
function sortComments(poemId, sortByRanking) {
var wrapper = document.getElementById('comments-list-wrapper');
if (wrapper == null) return;
$.ajax({
type: "GET",
url: '?handler=CommentsPartial&poemId=' + String(poemId) + '&sortByRanking=' + String(sortByRanking),
error: function (err) {
alert('خطا در بارگذاری حاشیه‌ها: ' + err.responseText);
},
success: function (html) {
wrapper.innerHTML = html;
paginateComments();
revealCommentFromUrlHash();
},
});
}
var COMMENTS_PAGE_SIZE = 15;
function paginateComments() {
var block = document.getElementById('comments-block');
if (block == null) return;
var existingButton = document.getElementById('comments-show-more');
if (existingButton != null) existingButton.remove();
// only direct children: root comments. Replies live nested inside each root's
// own div, so they're never touched by pagination - a full reply thread always
// stays together with its root.
var roots = Array.prototype.filter.call(block.children, function (el) {
return el.classList.contains('ganjoor-comment');
});
for (var i = 0; i < roots.length; i++) {
roots[i].style.display = '';
}
if (roots.length <= COMMENTS_PAGE_SIZE) return;
var shown = COMMENTS_PAGE_SIZE;
for (var i = shown; i < roots.length; i++) {
roots[i].style.display = 'none';
}
var button = document.createElement('a');
button.id = 'comments-show-more';
button.setAttribute('role', 'button');
button.className = 'cursor-pointer comments-link';
button.innerText = 'نمایش حاشیه‌های بیشتر (' + toPersianNumber(roots.length - shown) + ' مورد دیگر)';
button.onclick = function () {
var nextShown = shown + COMMENTS_PAGE_SIZE;
for (var i = shown; i < Math.min(nextShown, roots.length); i++) {
roots[i].style.display = '';
}
shown = nextShown;
if (shown >= roots.length) {
button.remove();
} else {
button.innerText = 'نمایش حاشیه‌های بیشتر (' + toPersianNumber(roots.length - shown) + ' مورد دیگر)';
}
};
block.parentNode.insertBefore(button, block.nextSibling);
}
// Hashieha.cshtml, MyComments.cshtml, and copyCommentUrl() all link straight to
// #comment-<id>. Pagination can hide that target (or, if it's a reply, hide its whole
// parent thread, since only root comments get display:none) - the browser's native
// anchor-scroll runs before paginateComments() and silently fails against a hidden
// element, so this reveals the right root and scrolls to it explicitly afterwards.
function revealCommentFromUrlHash() {
if (!/^#comment-\d/.test(location.hash)) return;
var target = document.querySelector(location.hash);
if (target == null) return;
var block = document.getElementById('comments-block');
if (block != null) {
var el = target;
while (el != null && el.parentElement !== block) {
el = el.parentElement;
}
if (el != null) {
el.style.display = '';
}
}
setTimeout(function () {
target.scrollIntoView();
}, 0);
}
function rateComment(commentId, value, loggedIn, divSuffix) {
if (!loggedIn) {
alert('برای رأی دادن به حاشیه‌ها لازم است با نام کاربری خود وارد گنجور شوید.');