diff --git a/randogoth.com/bbs/static/bbs.js b/randogoth.com/bbs/static/bbs.js index cc9c8c7..f6a8bb0 100644 --- a/randogoth.com/bbs/static/bbs.js +++ b/randogoth.com/bbs/static/bbs.js @@ -165,6 +165,11 @@ var BBS = {}; addEventListener('keydown', function (e) { if (e.metaKey || e.ctrlKey || e.altKey) return; + // Until the reply form existed there was nothing on the board to type + // into, so every letter was a command. Now `i` in a message box would + // navigate to the index mid-sentence. + var t = e.target; + if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; var k = e.key; clearTimeout(timer); @@ -226,3 +231,172 @@ var BBS = {}; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })(); + +/* Replies. + * + * A message base is a thing people answer, and until now this one could only be + * read. The comments come from a remark42 instance which serves three sites + * from one database; everything below talks to its API directly rather than + * loading its widget, because the widget is a React app in an iframe and would + * arrive with its own fonts, spacing and avatars. None of that can be made to + * look like 80 columns of CP437, and an iframe cannot inherit the page's font + * in any case. + * + * The rules are drawn the way the guestbook draws them, padded to exactly 80 + * cells, so a reply is indistinguishable from any other row on the board. + * + * Auth is anonymous and the token comes back in a header rather than a cookie, + * which is what lets this work from another origin at all: a cross-site cookie + * would be blocked outright by Safari and Firefox. + */ +(function () { + var W = 80; + + function init() { + var box = document.getElementById('replies'); + if (!box) return; + var host = box.dataset.host, site = box.dataset.site, url = box.dataset.url; + var token = null; + + function esc(s) { + return String(s).replace(/[&<>"]/g, function (c) { + return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; + }); + } + function fill(n) { return n > 0 ? new Array(n + 1).join('─') : ''; } + function span(cls, text) { return '' + esc(text) + ''; } + + /* `── 06 oct 26 · 14:09 ──`, lower case, as the listings elsewhere do it. */ + function stamp(iso) { + var d = new Date(iso); + if (isNaN(d)) return ''; + var mon = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'][d.getMonth()]; + function p(n) { return (n < 10 ? '0' : '') + n; } + return p(d.getDate()) + ' ' + mon + ' ' + String(d.getFullYear()).slice(2) + + ' · ' + p(d.getHours()) + ':' + p(d.getMinutes()); + } + + /* One rule, padded so the visible width is exactly W. */ + function rule(n, who, when) { + var num = (' ' + n).slice(-3) + ' '; + var left = '──[' + num + ']── ' + who; + var right = ' ' + when + ' ──'; + var gap = W - left.length - right.length; + if (gap < 1) { // a very long handle: trim it, never overflow + who = who.slice(0, Math.max(1, who.length + gap - 1)) + '…'; + left = '──[' + num + ']── ' + who; + gap = W - left.length - right.length; + } + return '
' + span('box', '──[') + span('count', num) +
+ span('box', ']── ') + span('area', who) + span('box', fill(gap)) +
+ span('date', ' ' + when + ' ') + span('box', '──') + '';
+ }
+
+ function header(count) {
+ var title = ' REPLIES ';
+ var tail = count === 1 ? '1 message ' : count + ' messages ';
+ var left = '┌─' + title;
+ var right = tail + '─┐';
+ return '' + esc(left) + fill(W - left.length - right.length) + esc(right) + ''; + } + + function render(list) { + var html = header(list.length); + list.forEach(function (c, i) { + html += rule(i + 1, c.user.name, stamp(c.time)); + html += '
' + fill(W) + '' + + ''; + } + + function say(msg) { var s = document.getElementById('rs'); if (s) s.textContent = msg; } + + function load() { + return fetch(host + '/api/v1/find?site=' + encodeURIComponent(site) + + '&url=' + encodeURIComponent(url) + '&format=tree') + .then(function (r) { return r.json(); }) + .then(function (d) { + render((d.comments || []).map(function (n) { return n.comment; })); + }) + .catch(function () { box.innerHTML = header(0) + + '
--- {{ SITENAME }} BBS
* Origin: randogoth.com (1:1/1)
+
+{#- Replies. A message base is a thing people answer, which is the one feature
+ the metaphor was missing. Filled by bbs.js from the comment server; without
+ JS the section simply is not there, which is honest, and the capsule never
+ sees it at all since Gemini has no way to render it. -#}
+{%- if COMMENTS_HOST %}
+
+{%- endif %}
{% endblock %}
diff --git a/randogoth.com/pelicanconf.py b/randogoth.com/pelicanconf.py
index b7f1455..a8c8bd7 100644
--- a/randogoth.com/pelicanconf.py
+++ b/randogoth.com/pelicanconf.py
@@ -98,3 +98,9 @@ MARKDOWN = {
DELETE_OUTPUT_DIRECTORY = True
CACHE_CONTENT = False
+
+# The comment server. One instance serves three sites, keyed on COMMENTS_SITE,
+# so this is the id rather than the hostname. Unset it and the replies section
+# disappears from every article, which is the off switch.
+COMMENTS_HOST = 'https://comments.randogoth.com'
+COMMENTS_SITE = 'randogoth'