diff --git a/CHANGES.rst b/CHANGES.rst index 45098918..925b5365 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,12 +12,14 @@ New Features - Add search for comments by URL in the admin interface (`#1000`_, pkvach) - Add CSS variables for better organization and flexibility (`#1001`_, pkvach) - Add support for comment search by Thread URL in admin interface (`#1020`_, pkvach) +- Add sorting option for comments (`#1005`_, pkvach) .. _#966: https://github.com/posativ/isso/pull/966 .. _#998: https://github.com/isso-comments/isso/pull/998 .. _#1000: https://github.com/isso-comments/isso/pull/1000 .. _#1001: https://github.com/isso-comments/isso/pull/1001 .. _#1020: https://github.com/isso-comments/isso/pull/1020 +.. _#1005: https://github.com/isso-comments/isso/pull/1005 Breaking Changes ^^^^^^^^^^^^^^^^ @@ -47,6 +49,7 @@ Bugfixes & Improvements - Change logging to include datetime and loglevel (`#1023`_, ix5) - Make 'text' field in 'comments' table NOT NULL and handling data migration (`#1019`_, pkvach) - Python 3.12 support (`#1015`_, ix5) +- Disable Postbox submit button on click, enable after response (`#993`_, pkvach) - Provide full control of allowed HTML elements via the configuration file (`#1007`_, pkvach) .. _#951: https://github.com/posativ/isso/pull/951 @@ -61,6 +64,7 @@ Bugfixes & Improvements .. _#1023: https://github.com/isso-comments/isso/pull/1023 .. _#1019: https://github.com/isso-comments/isso/pull/1019 .. _#1015: https://github.com/isso-comments/isso/pull/1015 +.. _#993: https://github.com/isso-comments/isso/pull/993 .. _#1007: https://github.com/isso-comments/isso/pull/1007 0.13.1.dev0 (2023-02-05) diff --git a/docs/docs/reference/client-config.rst b/docs/docs/reference/client-config.rst index 40a1e267..b940adf7 100644 --- a/docs/docs/reference/client-config.rst +++ b/docs/docs/reference/client-config.rst @@ -13,6 +13,7 @@ preferably in the script tag which embeds the JS: data-isso-max-comments-top="10" data-isso-max-comments-nested="5" data-isso-reveal-on-click="5" + data-isso-sorting="newest" data-isso-avatar="true" data-isso-avatar-bg="#f0f0f0" data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..." @@ -255,6 +256,21 @@ data-isso-reply-notifications-default-enabled .. versionadded:: 0.13 +.. _data-isso-sorting: + +data-isso-sorting + Sort thread comments by specified sorting method. + + Possible sorting methods: + + - ``newest``: Bring newest comments to the top + - ``oldest``: Bring oldest comments to the top + - ``upvotes``: Bring most liked comments to the top + + Default sorting is ``oldest``. + + .. versionadded:: 0.13.1 + Deprecated Client Settings -------------------------- diff --git a/isso/db/comments.py b/isso/db/comments.py index c41c77fe..48cbbfe6 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -238,11 +238,11 @@ def fetchall(self, mode=5, after=0, parent='any', order_by='id', yield dict(zip(fields_comments + fields_threads, item)) def fetch(self, uri, mode=5, after=0, parent='any', - order_by='id', asc=1, limit=None): + order_by='id', asc=1, limit=None, offset=0): """ Return comments for :param:`uri` with :param:`mode`. """ - sql = ['SELECT comments.* FROM comments INNER JOIN threads ON', + sql = ['SELECT comments.*, likes - dislikes AS karma FROM comments INNER JOIN threads ON', ' threads.uri=? AND comments.tid=threads.id AND (? | comments.mode) = ?', ' AND comments.created>?'] @@ -256,14 +256,18 @@ def fetch(self, uri, mode=5, after=0, parent='any', sql_args.append(parent) # custom sanitization - if order_by not in ['id', 'created', 'modified', 'likes', 'dislikes']: + if order_by not in ['id', 'created', 'modified', 'likes', 'dislikes', 'karma']: order_by = 'id' sql.append('ORDER BY ') sql.append(order_by) if not asc: sql.append(' DESC') - if limit: + if offset and limit: + sql.append('LIMIT ?,?') + sql_args.append(offset) + sql_args.append(limit) + elif limit: sql.append('LIMIT ?') sql_args.append(limit) @@ -350,7 +354,7 @@ def vote(self, upvote, id, remote_addr): return {'likes': likes + 1, 'dislikes': dislikes} return {'likes': likes, 'dislikes': dislikes + 1} - def reply_count(self, url, mode=5, after=0): + def reply_count(self, url, mode=5): """ Return comment count for main thread and all reply threads for one url. """ @@ -358,11 +362,10 @@ def reply_count(self, url, mode=5, after=0): sql = ['SELECT comments.parent,count(*)', 'FROM comments INNER JOIN threads ON', ' threads.uri=? AND comments.tid=threads.id AND', - ' (? | comments.mode = ?) AND', - ' comments.created > ?', + ' (? | comments.mode = ?)', 'GROUP BY comments.parent'] - return dict(self.db.execute(sql, [url, mode, mode, after]).fetchall()) + return dict(self.db.execute(sql, [url, mode, mode]).fetchall()) def count(self, *urls): """ diff --git a/isso/js/app/api.js b/isso/js/app/api.js index 90efa492..540b6712 100644 --- a/isso/js/app/api.js +++ b/isso/js/app/api.js @@ -138,17 +138,13 @@ var view = function(id, plain) { return deferred.promise; }; -var fetch = function(tid, limit, nested_limit, parent, lastcreated) { - if (typeof(limit) === 'undefined') { limit = "inf"; } - if (typeof(nested_limit) === 'undefined') { nested_limit = "inf"; } - if (typeof(parent) === 'undefined') { parent = null; } +var fetch = function({ tid, limit = "inf", nested_limit = "inf", parent = null, sort = "", offset = 0 }) { + var query_dict = { uri: tid || location(), sort, parent, offset }; - var query_dict = {uri: tid || location(), after: lastcreated, parent: parent}; - - if(limit !== "inf") { + if (limit !== "inf") { query_dict['limit'] = limit; } - if(nested_limit !== "inf"){ + if (nested_limit !== "inf") { query_dict['nested_limit'] = nested_limit; } diff --git a/isso/js/app/default_config.js b/isso/js/app/default_config.js index 68dd9fec..6a872aed 100644 --- a/isso/js/app/default_config.js +++ b/isso/js/app/default_config.js @@ -13,6 +13,7 @@ var default_config = { "max-comments-top": "inf", "max-comments-nested": 5, "reveal-on-click": 5, + "sorting": "oldest", "gravatar": false, "avatar": true, "avatar-bg": "#f0f0f0", diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index fbf958a3..c4b6a33f 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -92,40 +92,56 @@ var Postbox = function(parent) { // submit form, initialize optional fields with `null` and reset form. // If replied to a comment, remove form completely. - $("[type=submit]", el).on("click", function() { + $("[type=submit]", el).on("click", function(event) { edit(); if (! el.validate()) { return; } + const submitButton = event.target; + submitButton.disabled = true; // Disable the submit button to prevent double posting + var author = $("[name=author]", el).value || null, email = $("[name=email]", el).value || null, website = $("[name=website]", el).value || null; - localStorage.setItem("isso-author", JSON.stringify(author)); - localStorage.setItem("isso-email", JSON.stringify(email)); - localStorage.setItem("isso-website", JSON.stringify(website)); - - api.create($("#isso-thread").getAttribute("data-isso-id"), { - author: author, email: email, website: website, - text: $(".isso-textarea", el).value, - parent: parent || null, - title: $("#isso-thread").getAttribute("data-title") || null, - notification: $("[name=notification]", el).checked() ? 1 : 0, - }).then(function(comment) { - $(".isso-textarea", el).value = ""; - insert(comment, true); - - if (parent !== null) { - el.onsuccess(); - } - }); + try { + localStorage.setItem("isso-author", JSON.stringify(author)); + localStorage.setItem("isso-email", JSON.stringify(email)); + localStorage.setItem("isso-website", JSON.stringify(website)); + + api.create($("#isso-thread").getAttribute("data-isso-id"), { + author: author, email: email, website: website, + text: $(".isso-textarea", el).value, + parent: parent || null, + title: $("#isso-thread").getAttribute("data-title") || null, + notification: $("[name=notification]", el).checked() ? 1 : 0, + }).then( + function(comment) { + $(".isso-textarea", el).value = ""; + insert({ comment, scrollIntoView: true, offset: 0 }); + + if (parent !== null) { + el.onsuccess(); + } + + submitButton.disabled = false; + }, + function(err) { + console.error(err); + submitButton.disabled = false; + } + ); + } catch (err) { + console.error(err); + submitButton.disabled = false; + } }); return el; }; -var insert_loader = function(comment, lastcreated) { +var insert_loader = function(comment, offset) { var entrypoint; if (comment.id === null) { entrypoint = $("#isso-root"); @@ -140,34 +156,37 @@ var insert_loader = function(comment, lastcreated) { $("a.isso-load-hidden", el).on("click", function() { el.remove(); - api.fetch($("#isso-thread").getAttribute("data-isso-id"), - config["reveal-on-click"], config["max-comments-nested"], - comment.id, - lastcreated).then( + + api.fetch({ + tid: $("#isso-thread").getAttribute("data-isso-id"), + limit: config["reveal-on-click"], + nested_limit: config["max-comments-nested"], + parent: comment.id, + sort: config["sorting"], + offset: offset + }).then( function(rv) { if (rv.total_replies === 0) { return; } - var lastcreated = 0; rv.replies.forEach(function(commentObject) { - insert(commentObject, false); - if(commentObject.created > lastcreated) { - lastcreated = commentObject.created; - } + insert({ comment: commentObject, scrollIntoView: false, offset: 0 }); + }); if(rv.hidden_replies > 0) { - insert_loader(rv, lastcreated); + insert_loader(rv, offset + rv.replies.length); } }, function(err) { console.log(err); - }); + } + ); }); }; -var insert = function(comment, scrollIntoView) { +var insert = function({ comment, scrollIntoView, offset }) { var el = $.htmlify(template.render("comment", {"comment": comment})); // update datetime every 60 seconds @@ -381,19 +400,13 @@ var insert = function(comment, scrollIntoView) { show($("a.isso-reply", footer).detach()); } - if(comment.hasOwnProperty('replies')) { - var lastcreated = 0; - comment.replies.forEach(function(replyObject) { - insert(replyObject, false); - if(replyObject.created > lastcreated) { - lastcreated = replyObject.created; - } - + if (comment.hasOwnProperty('replies')) { + comment.replies.forEach(function (replyObject) { + insert({ comment: replyObject, scrollIntoView: false, offset: offset + 1 }); }); - if(comment.hidden_replies > 0) { - insert_loader(comment, lastcreated); + if (comment.hidden_replies > 0) { + insert_loader(comment, offset + comment.replies.length); } - } }; diff --git a/isso/js/embed.js b/isso/js/embed.js index 1c1cfb8e..dd7b06af 100644 --- a/isso/js/embed.js +++ b/isso/js/embed.js @@ -122,9 +122,15 @@ function fetchComments() { } isso_root.textContent = ''; - api.fetch(isso_thread.getAttribute("data-isso-id") || location.pathname, - config["max-comments-top"], - config["max-comments-nested"]).then( + + api.fetch({ + tid: isso_thread.getAttribute("data-isso-id") || location.pathname, + limit: config["max-comments-top"], + nested_limit: config["max-comments-nested"], + parent: null, + sort: config["sorting"], + offset: 0 + }).then( function (rv) { if (rv.total_replies === 0) { @@ -132,18 +138,14 @@ function fetchComments() { return; } - var lastcreated = 0; var count = rv.total_replies; rv.replies.forEach(function(comment) { - isso.insert(comment, false); - if (comment.created > lastcreated) { - lastcreated = comment.created; - } + isso.insert({ comment: comment, scrollIntoView: false, offset: 0 }); }); heading.textContent = i18n.pluralize("num-comments", count); if (rv.hidden_replies > 0) { - isso.insert_loader(rv, lastcreated); + isso.insert_loader(rv, rv.replies.length); } if (window.location.hash.length > 0 && diff --git a/isso/js/tests/integration/highlight-comments.test.js b/isso/js/tests/integration/highlight-comments.test.js index 0e176c11..4f21cbe9 100644 --- a/isso/js/tests/integration/highlight-comments.test.js +++ b/isso/js/tests/integration/highlight-comments.test.js @@ -40,6 +40,7 @@ test('Linked should be highlighted', async () => { // Cleanup // Need to click once to surface "confirm" and then again to confirm + await page.waitForSelector('#isso-1 > .isso-text-wrapper > .isso-comment-footer > .isso-delete'); await expect(page).toClick('#isso-1 > .isso-text-wrapper > .isso-comment-footer > .isso-delete'); await expect(page).toClick('#isso-1 > .isso-text-wrapper > .isso-comment-footer > .isso-delete'); }); diff --git a/isso/js/tests/integration/puppet.test.js b/isso/js/tests/integration/puppet.test.js index 5b55caa1..86ae6dc7 100644 --- a/isso/js/tests/integration/puppet.test.js +++ b/isso/js/tests/integration/puppet.test.js @@ -148,6 +148,8 @@ test("should fill Postbox with valid data and receive 201 reply", async () => { await expect(elm.replace(//, '