diff --git a/CHANGES.rst b/CHANGES.rst index f98982f3..c1f71852 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,8 +8,11 @@ New Features ^^^^^^^^^^^^ - Add option to show/hide website field in comment form (`#1111`_, pkvach) +- Client: Add ``data-isso-read-only`` client option to render threads in + read-only mode (hides postbox and disables reply/edit/delete UI). (`#1104`_, pkvach) .. _#1111: https://github.com/isso-comments/isso/pull/1111 +.. _#1104: https://github.com/isso-comments/isso/pull/1104 0.14.0 (2026-03-26) -------------------- diff --git a/docs/docs/reference/client-config.rst b/docs/docs/reference/client-config.rst index 762d9e9e..f261c415 100644 --- a/docs/docs/reference/client-config.rst +++ b/docs/docs/reference/client-config.rst @@ -272,6 +272,30 @@ data-isso-sorting .. versionadded:: 0.13.1 +.. _data-isso-read-only: + +data-isso-read-only + Set to ``true`` to display the comment thread in read-only mode. This will + hide the main comment postbox and disable reply/edit/delete functionality, + showing only existing comments. + + This is useful for archived content, closed discussions, or when you want + to display comments without allowing new submissions. + + .. note:: + + This is a **client-side only** setting that affects the UI display but + does not prevent direct API calls. It disables the postbox, reply, + edit, and delete links while keeping voting and comment display active. + + .. code-block:: html + + + + Default: ``false`` + + .. versionadded:: 0.14.1 + Deprecated Client Settings -------------------------- diff --git a/isso/js/app/default_config.js b/isso/js/app/default_config.js index 6a872aed..b1a1e0a1 100644 --- a/isso/js/app/default_config.js +++ b/isso/js/app/default_config.js @@ -23,6 +23,7 @@ var default_config = { "vote-levels": null, "feed": false, "page-author-hashes": "", + "read-only": false, }; Object.freeze(default_config); diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index 5471c394..b3649ea7 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -222,7 +222,17 @@ var insert = function({ comment, scrollIntoView, offset }) { text = $("#isso-" + comment.id + " > .isso-text-wrapper > .isso-text"); var form = null; // XXX: probably a good place for a closure - $("a.isso-reply", footer).toggle("click", + + // Helper function to attach toggle handlers to buttons with null checks + var attachToggleHandler = function(selector, onActivate, onDeactivate) { + var button = $(selector, footer); + if (button) { + button.toggle("click", onActivate, onDeactivate); + } + }; + + // Reply button handler + attachToggleHandler("a.isso-reply", function(toggler) { form = footer.insertAfter(new Postbox(comment.parent === null ? comment.id : comment.parent)); form.onsuccess = function() { toggler.next(); }; @@ -283,7 +293,8 @@ var insert = function({ comment, scrollIntoView, offset }) { votes(comment.likes - comment.dislikes); } - $("a.isso-edit", footer).toggle("click", + // Edit button handler + attachToggleHandler("a.isso-edit", function(toggler) { var edit = $("a.isso-edit", footer); var avatar = config["avatar"] || config["gravatar"] ? $(".isso-avatar", el, false)[0] : null; @@ -346,7 +357,8 @@ var insert = function({ comment, scrollIntoView, offset }) { } ); - $("a.isso-delete", footer).toggle("click", + // Delete button handler + attachToggleHandler("a.isso-delete", function(toggler) { var del = $("a.isso-delete", footer); var state = ! toggler.state; diff --git a/isso/js/app/templates/comment.js b/isso/js/app/templates/comment.js index f88be943..5072c168 100644 --- a/isso/js/app/templates/comment.js +++ b/isso/js/app/templates/comment.js @@ -40,9 +40,12 @@ var html = function (globals) { + "|" + "" + svg['arrow-down'] + "" : '') - + "" + i18n('comment-reply') + "" - + "" + i18n('comment-edit') + "" - + "" + i18n('comment-delete') + "" + + (conf["read-only"] + ? '' + : "" + i18n('comment-reply') + "" + + "" + i18n('comment-edit') + "" + + "" + i18n('comment-delete') + "" + ) + "" // .isso-comment-footer + "" // .text-wrapper + "
" diff --git a/isso/js/embed.js b/isso/js/embed.js index dd7b06af..5a128e63 100644 --- a/isso/js/embed.js +++ b/isso/js/embed.js @@ -78,11 +78,13 @@ function init() { if (!$('h4.isso-thread-heading')) { isso_thread.append(heading); } - postbox = new isso.Postbox(null); - if (!$('.isso-postbox')) { - isso_thread.append(postbox); - } else { - $('.isso-postbox').value = postbox; + if (!config["read-only"]) { + postbox = new isso.Postbox(null); + if (!$('.isso-postbox')) { + isso_thread.append(postbox); + } else { + $('.isso-postbox').value = postbox; + } } if (!$('#isso-root')) { isso_thread.append('
'); diff --git a/isso/js/tests/unit/read-only-mode.test.js b/isso/js/tests/unit/read-only-mode.test.js new file mode 100644 index 00000000..bed7dc76 --- /dev/null +++ b/isso/js/tests/unit/read-only-mode.test.js @@ -0,0 +1,86 @@ +/** + * @jest-environment jsdom + */ + +"use strict"; + +describe('Isso read-only mode', () => { + beforeEach(() => { + jest.resetModules(); + + // globals.offset.localTime() will be passed to i18n.ago() + // localTime param will then be called as localTime.getTime() + jest.mock('app/globals', () => ({ + offset: { + localTime: jest.fn(() => ({ + getTime: jest.fn(() => 0), + })), + }, + })); + + document.body.innerHTML = + '
' + + ''; + }); + + test('should not render postbox in read-only mode', () => { + const $ = require("app/dom"); + const config = require("app/config"); + const template = require("app/template"); + const i18n = require("app/i18n"); + const svg = require("app/svg"); + + template.set("conf", config); + template.set("i18n", i18n.translate); + template.set("pluralize", i18n.pluralize); + template.set("svg", svg); + + let isso_thread = $('#isso-thread'); + isso_thread.append('
'); + + expect($('.isso-postbox')).toBeNull(); + }); + + test('should not render reply, edit, and delete buttons in read-only mode', () => { + const isso = require("app/isso"); + const $ = require("app/dom"); + const config = require("app/config"); + const template = require("app/template"); + const i18n = require("app/i18n"); + const svg = require("app/svg"); + + template.set("conf", config); + template.set("i18n", i18n.translate); + template.set("pluralize", i18n.pluralize); + template.set("svg", svg); + + let isso_thread = $('#isso-thread'); + isso_thread.append('
'); + + // Simulate a comment object + let comment = { + id: 1, + hash: "abc123", + author: "TestUser", + website: null, + created: 1651788192.4473603, + mode: 1, + text: "Test comment", + likes: 0, + dislikes: 0, + replies: [], + hidden_replies: 0, + parent: null + }; + + // Render comment + isso.insert({comment, scrollIntoView: false, offset: 0}); + + // Verify that interactive buttons are not rendered in read-only mode + expect($('a.isso-reply')).toBeNull(); + expect($('a.isso-edit')).toBeNull(); + expect($('a.isso-delete')).toBeNull(); + }); +});