feat: add QR codes for tribe invites, pub invites and user profile sharing
- tribes_view: renderInvitePage now shows QR of the invite code - invites_view: snhInvite box shows QR of pub invite code - inhabitants_view: user profile shows QR of SSB ID (own card + profile view) - style.css: add QR code styles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f88a179711
commit
54ad8a12fc
21655 changed files with 2101340 additions and 0 deletions
1400
nodejs-project/src/views/courts_view.js
Normal file
1400
nodejs-project/src/views/courts_view.js
Normal file
File diff suppressed because it is too large
Load diff
161
nodejs-project/src/views/invites_view.js
Normal file
161
nodejs-project/src/views/invites_view.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
const { form, button, div, h2, h3, p, span, section, ul, li, a, br, hr, input, table, tr, td } = require("../server/node_modules/hyperaxe");
|
||||
const path = require("path");
|
||||
const fs = require('fs');
|
||||
const { template, i18n } = require('./main_views');
|
||||
|
||||
const homedir = require('os').homedir();
|
||||
const gossipPath = path.join(homedir, ".ssb", "gossip.json");
|
||||
const unfollowedPath = path.join(homedir, ".ssb", "gossip_unfollowed.json");
|
||||
|
||||
const encodePubLink = (key) => {
|
||||
let core = String(key).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/');
|
||||
if (!core.endsWith('=')) core += '=';
|
||||
return `/author/${encodeURIComponent('@' + core)}.ed25519`;
|
||||
};
|
||||
|
||||
|
||||
const deduplicateByHost = (list) => {
|
||||
const seen = new Set();
|
||||
return list.filter(p => {
|
||||
const host = (p.host || '').replace(/:\d+$/, '');
|
||||
if (!host || seen.has(host)) return false;
|
||||
seen.add(host);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const invitesView = ({ invitesEnabled }) => {
|
||||
let pubs = [];
|
||||
let pubsValue = "false";
|
||||
let unfollowed = [];
|
||||
|
||||
try {
|
||||
pubs = fs.readFileSync(gossipPath, "utf8");
|
||||
} catch {
|
||||
pubs = '[]';
|
||||
}
|
||||
|
||||
try {
|
||||
pubs = JSON.parse(pubs);
|
||||
pubsValue = Array.isArray(pubs) && pubs.length > 0 ? "true" : "false";
|
||||
} catch {
|
||||
pubsValue = "false";
|
||||
pubs = [];
|
||||
}
|
||||
|
||||
try {
|
||||
unfollowed = JSON.parse(fs.readFileSync(unfollowedPath, "utf8") || "[]");
|
||||
} catch {
|
||||
unfollowed = [];
|
||||
}
|
||||
|
||||
const filteredPubs = pubsValue === "true"
|
||||
? deduplicateByHost(pubs.filter(pubItem => !unfollowed.find(u => u.key === pubItem.key)))
|
||||
: [];
|
||||
|
||||
const hasError = (pubItem) => pubItem && (pubItem.error || (typeof pubItem.failure === 'number' && pubItem.failure > 0));
|
||||
|
||||
const unreachableLabel = i18n.currentlyUnreachable || i18n.currentlyUnrecheable || 'ERROR!';
|
||||
|
||||
const pubTableHeader = () => tr(
|
||||
td({ class: 'card-label' }, 'PUB'),
|
||||
td({ class: 'card-label' }, i18n.invitesPort || 'Port'),
|
||||
td({ class: 'card-label' }, i18n.inhabitants),
|
||||
td({ class: 'card-label' }, 'Key'),
|
||||
td({ class: 'card-label' }, '')
|
||||
);
|
||||
|
||||
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 title = i18n.invites;
|
||||
const description = i18n.invitesDescription;
|
||||
|
||||
return template(
|
||||
title,
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
h2(title),
|
||||
p(description)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-tribes' },
|
||||
h2(i18n.invitesTribesTitle),
|
||||
form(
|
||||
{ action: '/tribes/join-code', method: 'post' },
|
||||
input({ name: 'inviteCode', type: 'text', placeholder: i18n.invitesTribeInviteCodePlaceholder, autofocus: true, required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesTribeJoinButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'pubs-section' },
|
||||
h2(i18n.invitesPubsTitle),
|
||||
div({ class: 'card' },
|
||||
h3('SNH "La Plaza"'),
|
||||
p(span({ class: 'card-label' }, 'solarnethub.com:8008')),
|
||||
form(
|
||||
{ action: '/settings/invite/accept', method: 'post' },
|
||||
input({ name: 'invite', type: 'hidden', value: 'solarnethub.com:8008:@zGfPCNPFas4gHUfib08/oQ4rsWo/tnEfQ5iTkoTiBaI=.ed25519~5qLNt94SWwfXwLFBSco0axXLJ1g7640QULTvC2t2eNk=' }),
|
||||
button({ type: 'submit' }, i18n.invitesAcceptInvite)
|
||||
)
|
||||
),
|
||||
hr(),
|
||||
form(
|
||||
{ action: '/settings/invite/accept', method: 'post' },
|
||||
input({ name: 'invite', type: 'text', placeholder: i18n.invitesPubInviteCodePlaceholder, autofocus: true, required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesAcceptInvite)
|
||||
),
|
||||
hr(),
|
||||
h2(`${i18n.invitesAcceptedInvites} (${activePubs.length})`),
|
||||
activePubs.length
|
||||
? renderPubTable(activePubs, pubItem =>
|
||||
form({ action: '/settings/invite/unfollow', method: 'post' },
|
||||
input({ type: 'hidden', name: 'key', value: pubItem.key }),
|
||||
button({ type: 'submit' }, i18n.invitesUnfollow)
|
||||
)
|
||||
)
|
||||
: p(i18n.invitesNoFederatedPubs),
|
||||
hr(),
|
||||
h2(`${i18n.invitesUnfollowedInvites} (${unfollowed.length})`),
|
||||
unfollowed.length
|
||||
? 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 || '' }),
|
||||
input({ type: 'hidden', name: 'port', value: String(pubItem.port || 8008) }),
|
||||
button({ type: 'submit', disabled: hasError(pubItem) }, i18n.invitesFollow)
|
||||
)
|
||||
)
|
||||
: p(i18n.invitesNoUnfollowed),
|
||||
hr(),
|
||||
h2(`${i18n.invitesUnreachablePubs} (${unreachablePubs.length})`),
|
||||
unreachablePubs.length
|
||||
? renderPubTable(unreachablePubs, pubItem =>
|
||||
div({ class: 'error-box' },
|
||||
p({ class: 'error-title' }, i18n.errorDetails),
|
||||
p({ class: 'error-pre' }, String(pubItem.error || i18n.genericError))
|
||||
)
|
||||
)
|
||||
: p(i18n.invitesNoUnreachablePubs)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
exports.invitesView = invitesView;
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue