feat: add QR share button to each pub row in invites view

- Import details, summary from hyperaxe
- renderPubTable converted to async, generates QR SVG per pub key
- Each row: details/summary collapsible QR panel below the key link
  (same pattern as profile and tribe invite QR, no JS required)
- All three renderPubTable calls updated with await
- QR falls back silently if key is missing or generation fails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SITO 2026-05-01 02:33:02 +02:00
parent 16f9189e61
commit 12acd6fd20

View file

@ -1,4 +1,4 @@
const { form, button, div, h2, h3, p, section, ul, li, a, br, hr, input, span, table, tr, td } = require("../server/node_modules/hyperaxe");
const { form, button, div, h2, h3, p, section, ul, li, a, br, hr, input, span, table, tr, td, details, summary } = require("../server/node_modules/hyperaxe");
const QRCode = require('../server/node_modules/qrcode');
const path = require("path");
const fs = require('fs');
@ -75,16 +75,31 @@ const invitesView = async ({ invitesEnabled }) => {
const activePubs = filteredPubs.filter(pubItem => !hasError(pubItem));
const unreachablePubs = pubs.filter(hasError);
const renderPubTable = (items, actionFn) => table({ class: 'block-info-table' },
pubTableHeader(),
items.map(pubItem => tr(
td(pubItem.host || '—'),
td(String(pubItem.port || 8008)),
td(String(pubItem.announcers || 0)),
td(a({ href: encodePubLink(pubItem.key), class: 'user-link' }, pubItem.key)),
td(actionFn(pubItem))
))
);
const renderPubTable = async (items, actionFn) => {
const rows = await Promise.all(items.map(async pubItem => {
let qrSvg = '';
try {
if (pubItem.key) qrSvg = await QRCode.toString(pubItem.key, { type: 'svg' });
} catch {}
return tr(
td(pubItem.host || '—'),
td(String(pubItem.port || 8008)),
td(String(pubItem.announcers || 0)),
td(
a({ href: encodePubLink(pubItem.key), class: 'user-link' }, pubItem.key),
qrSvg ? details({ class: 'qr-share-details' },
summary({ class: 'qr-share-btn' }, '⬡ QR'),
div({ class: 'qr-share-panel' },
div({ class: 'qr-code', innerHTML: qrSvg }),
p({ class: 'qr-share-id' }, pubItem.key)
)
) : null
),
td(actionFn(pubItem))
);
}));
return table({ class: 'block-info-table' }, pubTableHeader(), ...rows);
};
const title = i18n.invites;
const description = i18n.invitesDescription;
@ -130,7 +145,7 @@ const invitesView = async ({ invitesEnabled }) => {
hr(),
h2(`${i18n.invitesAcceptedInvites} (${activePubs.length})`),
activePubs.length
? renderPubTable(activePubs, pubItem =>
? await renderPubTable(activePubs, pubItem =>
form({ action: '/settings/invite/unfollow', method: 'post' },
input({ type: 'hidden', name: 'key', value: pubItem.key }),
button({ type: 'submit' }, i18n.invitesUnfollow)
@ -140,7 +155,7 @@ const invitesView = async ({ invitesEnabled }) => {
hr(),
h2(`${i18n.invitesUnfollowedInvites} (${unfollowed.length})`),
unfollowed.length
? renderPubTable(unfollowed, pubItem =>
? await renderPubTable(unfollowed, pubItem =>
form({ action: '/settings/invite/follow', method: 'post' },
input({ type: 'hidden', name: 'key', value: pubItem.key }),
input({ type: 'hidden', name: 'host', value: pubItem.host || '' }),
@ -152,7 +167,7 @@ const invitesView = async ({ invitesEnabled }) => {
hr(),
h2(`${i18n.invitesUnreachablePubs} (${unreachablePubs.length})`),
unreachablePubs.length
? renderPubTable(unreachablePubs, pubItem =>
? await renderPubTable(unreachablePubs, pubItem =>
div({ class: 'error-box' },
p({ class: 'error-title' }, i18n.errorDetails),
p({ class: 'error-pre' }, String(pubItem.error || i18n.genericError))