upstream: integrar Oasis 1.3.1
Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.
Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).
Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
This commit is contained in:
parent
8d838dd8b1
commit
c332afd6ea
265 changed files with 80970 additions and 29167 deletions
|
|
@ -1,12 +1,13 @@
|
|||
const { form, button, div, h2, h3, p, section, ul, li, a, br, hr, input, label, span, table, tr, td, textarea } = require("../server/node_modules/hyperaxe");
|
||||
const { form, button, div, h2, h3, p, section, a, br, input, label, table, tr, td, textarea } = require("../server/node_modules/hyperaxe");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const path = require("path");
|
||||
const fs = require('fs');
|
||||
const { renderUrl } = require("../backend/renderUrl");
|
||||
const { renderStyledText } = require("../backend/renderStyledText");
|
||||
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 gossipPath = path.join(require("../server/ssb_config").path, "gossip.json");
|
||||
const unfollowedPath = require("../configs/state-manager").statePath("gossip_unfollowed.json");
|
||||
const peerHealth = require("../models/peer_health");
|
||||
|
||||
const encodePubLink = (key) => {
|
||||
let core = String(key).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
|
@ -14,12 +15,21 @@ const encodePubLink = (key) => {
|
|||
return `/author/${encodeURIComponent('@' + core)}.ed25519`;
|
||||
};
|
||||
|
||||
const snhInvitePath = path.join(__dirname, '..', 'configs', 'snh-invite-code.json');
|
||||
|
||||
let snhInvite = null;
|
||||
try {
|
||||
snhInvite = JSON.parse(fs.readFileSync(snhInvitePath, 'utf8'));
|
||||
} catch {}
|
||||
const INVITE_KIND_LABEL = {
|
||||
tribes: 'invitesTribesTitle',
|
||||
houses: 'invitesHousesTitle',
|
||||
chats: 'invitesChatsTitle',
|
||||
pads: 'invitesPadsTitle',
|
||||
rooms: 'invitesRoomsTitle',
|
||||
calendars: 'invitesCalendarsTitle',
|
||||
events: 'invitesEventsTitle',
|
||||
forum: 'invitesForumsTitle',
|
||||
school: 'invitesSchoolTitle',
|
||||
maps: 'invitesMapsTitle',
|
||||
shops: 'invitesShopsTitle',
|
||||
inhabitants: 'invitesInhabitantsTitle',
|
||||
pubs: 'invitesPubsTitle'
|
||||
};
|
||||
|
||||
const deduplicateByHost = (list) => {
|
||||
const seen = new Set();
|
||||
|
|
@ -31,7 +41,25 @@ const deduplicateByHost = (list) => {
|
|||
});
|
||||
};
|
||||
|
||||
const invitesView = ({ invitesEnabled, flash }) => {
|
||||
const renderSuggestedPub = (entry, canJoin) => div({ class: 'snh-invite-box' },
|
||||
h3({ class: 'snh-invite-name' }, entry.name),
|
||||
p({ class: 'snh-invite-name' }, entry.description),
|
||||
p({ class: 'snh-invite-name' }, renderStyledText(entry.url)),
|
||||
entry.createdAt ? p({ class: 'snh-invite-date' }, `${i18n.statsCreatedAt || 'Created'}: ${moment(entry.createdAt).format("YYYY/MM/DD")}`) : null,
|
||||
p({ class: 'snh-invite-name snh-invite-code' }, entry.code),
|
||||
entry.invites ? p({ class: 'snh-invite-date' }, `${i18n.invitesPubUses}: ${entry.invites.used}/${entry.invites.total}`) : null,
|
||||
entry.exhausted
|
||||
? p({ class: 'snh-invite-name' }, i18n.invitesPubExhausted)
|
||||
: canJoin
|
||||
? form({ action: '/settings/invite/accept', method: 'post' },
|
||||
input({ type: 'hidden', name: 'invite', value: entry.code }),
|
||||
button({ type: 'submit' }, i18n.invitesAcceptInvite)
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const invitesView = (opts = {}) => {
|
||||
const canJoin = opts.canJoin !== false;
|
||||
let pubs = [];
|
||||
let pubsValue = "false";
|
||||
let unfollowed = [];
|
||||
|
|
@ -60,15 +88,8 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
? 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 sanitizeError = (err) => {
|
||||
if (!err) return i18n.genericError || 'Unknown error';
|
||||
const firstLine = String(err).split('\n')[0].replace(/^Error:\s*/, '').trim();
|
||||
return firstLine || (i18n.genericError || 'Unknown error');
|
||||
};
|
||||
|
||||
const unreachableLabel = i18n.currentlyUnreachable || i18n.currentlyUnrecheable || 'ERROR!';
|
||||
const deadKeys = opts.deadKeys instanceof Set ? opts.deadKeys : new Set();
|
||||
const hasError = (pubItem) => !!(pubItem && deadKeys.has(peerHealth.canonicalKey(pubItem.key)));
|
||||
|
||||
const pubTableHeader = () => tr(
|
||||
td({ class: 'card-label' }, 'PUB'),
|
||||
|
|
@ -78,7 +99,9 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
);
|
||||
|
||||
const activePubs = filteredPubs.filter(pubItem => !hasError(pubItem));
|
||||
const unreachablePubs = pubs.filter(hasError);
|
||||
const filter = opts.filter === 'others' ? 'others' : 'pubs';
|
||||
const suggested = Array.isArray(opts.inviteCodes) ? opts.inviteCodes : [];
|
||||
const used = (Array.isArray(opts.invitesUsed) ? opts.invitesUsed : []).filter((e) => e && typeof e.href === 'string' && e.href.startsWith('/'));
|
||||
|
||||
const renderPubTable = (items, actionFn) => table({ class: 'block-info-table invites-pubs-table' },
|
||||
pubTableHeader(),
|
||||
|
|
@ -96,56 +119,44 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
return template(
|
||||
title,
|
||||
section(
|
||||
div({ class: 'tags-header' },
|
||||
div({ class: 'tags-header module-header-line' },
|
||||
h2(title),
|
||||
p(description)
|
||||
),
|
||||
div({ class: 'mode-buttons-row' },
|
||||
a({ class: filter === 'pubs' ? 'filter-btn active' : 'filter-btn', href: '/invites?filter=pubs' }, String(i18n.invitesPubsTitle).toUpperCase()),
|
||||
a({ class: filter === 'others' ? 'filter-btn active' : 'filter-btn', href: '/invites?filter=others' }, String(i18n.invitesFilterOthers).toUpperCase())
|
||||
)
|
||||
),
|
||||
flash === 'alreadyFederated'
|
||||
? section(div({ class: 'message-banner' }, p(i18n.invitesAlreadyFederated || 'You are already federated with this pub.')))
|
||||
: null,
|
||||
section(
|
||||
filter === 'pubs' && suggested.length ? section(
|
||||
div({ class: 'pubs-section' },
|
||||
h2(`${i18n.invitesSuggestedPubs} (${suggested.length})`),
|
||||
...suggested.map((entry) => renderSuggestedPub(entry, canJoin))
|
||||
)
|
||||
) : null,
|
||||
filter === 'pubs' ? section(
|
||||
div({ class: 'pubs-section' },
|
||||
h2(i18n.invitesPubsTitle),
|
||||
p(i18n.invitesPubsHint),
|
||||
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)
|
||||
),
|
||||
br(),
|
||||
snhInvite ? div({ class: 'snh-invite-box' },
|
||||
h3({ class: 'snh-invite-name' }, snhInvite.name),
|
||||
p({ class: 'snh-invite-name' }, snhInvite.description),
|
||||
p({ class: 'snh-invite-name' }, renderUrl(snhInvite.url)),
|
||||
snhInvite.createdAt ? p({ class: 'snh-invite-date' }, `${i18n.statsCreatedAt || 'Created'}: ${new Date(snhInvite.createdAt).toLocaleDateString()}`) : null,
|
||||
form({ action: '/settings/invite/accept', method: 'post' },
|
||||
input({ type: 'hidden', name: 'invite', value: snhInvite.code }),
|
||||
button({ type: 'submit', class: 'filter-btn' }, snhInvite.code)
|
||||
)
|
||||
) : null
|
||||
canJoin
|
||||
? 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)
|
||||
)
|
||||
: p({ class: 'invites-join-note' }, opts.publicMode === true ? i18n.invitesJoinPubMode : i18n.invitesJoinLocalOnly)
|
||||
)
|
||||
),
|
||||
section(
|
||||
) : null,
|
||||
filter === 'pubs' ? section(
|
||||
div({ class: 'federations-section' },
|
||||
h2(i18n.invitesFederationsTitle || 'Federations'),
|
||||
p(i18n.invitesFederationsHint),
|
||||
div({ class: 'conn-actions invites-pubs-actions' },
|
||||
form({ action: '/invites/refresh-pubs', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsRefresh || 'Refresh')),
|
||||
form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Remove unreachable')),
|
||||
form({ action: '/invites/clear-unreachable', method: 'post' }, button({ type: 'submit' }, i18n.invitesPubsClearUnreachable || 'Clean')),
|
||||
form({ action: '/invites/export-pubs', method: 'get' }, button({ type: 'submit' }, i18n.invitesPubsExport || 'Export'))
|
||||
),
|
||||
form(
|
||||
{ action: '/invites/import-pubs', method: 'post', enctype: 'multipart/form-data', class: 'peers-import-form' },
|
||||
label({ class: 'peers-import-label' }, i18n.invitesPubsImportTitle || 'Import pubs'),
|
||||
br(),
|
||||
textarea({ name: 'peerList', rows: '4', placeholder: i18n.invitesPubsImportPlaceholder || 'Paste one multiserver address or invite code per line…' }),
|
||||
br(),
|
||||
input({ type: 'file', name: 'peerFile', accept: '.txt,text/plain' }),
|
||||
br(),
|
||||
button({ type: 'submit', class: 'filter-btn' }, i18n.invitesPubsImport || 'Import')
|
||||
),
|
||||
h2(`${i18n.invitesAcceptedInvites} (${activePubs.length})`),
|
||||
activePubs.length
|
||||
? renderPubTable(activePubs, pubItem =>
|
||||
|
|
@ -155,7 +166,6 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
)
|
||||
)
|
||||
: p(i18n.invitesNoFederatedPubs),
|
||||
hr(),
|
||||
h2(`${i18n.invitesUnfollowedInvites} (${unfollowed.length})`),
|
||||
unfollowed.length
|
||||
? renderPubTable(unfollowed, pubItem =>
|
||||
|
|
@ -167,145 +177,51 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
)
|
||||
)
|
||||
: 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' }, sanitizeError(pubItem.error))
|
||||
)
|
||||
form(
|
||||
{ action: '/invites/import-pubs', method: 'post', enctype: 'multipart/form-data', class: 'peers-import-form' },
|
||||
label({ class: 'peers-import-label' }, i18n.invitesPubsImportTitle || 'Import pubs'),
|
||||
br(),
|
||||
textarea({ name: 'peerList', rows: '4', placeholder: i18n.invitesPubsImportPlaceholder || 'Paste one multiserver address or invite code per line…' }),
|
||||
br(),
|
||||
input({ type: 'file', name: 'peerFile', accept: '.txt,text/plain' }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesPubsImport || 'Import')
|
||||
)
|
||||
)
|
||||
) : null,
|
||||
filter === 'others' ? section(
|
||||
div(
|
||||
h2(i18n.invitesJoinTitle),
|
||||
p(i18n.invitesJoinHint),
|
||||
form(
|
||||
{ action: '/invites/join', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesJoinPlaceholder, required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesJoinButton)
|
||||
),
|
||||
h2(`${i18n.invitesUsedTitle} (${used.length})`),
|
||||
used.length
|
||||
? table({ class: 'block-info-table invites-used-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.invitesUsedType),
|
||||
td({ class: 'card-label' }, i18n.invitesUsedName),
|
||||
td({ class: 'card-label' }, i18n.invitesUsedDate)
|
||||
),
|
||||
used.map((entry) => tr(
|
||||
td(INVITE_KIND_LABEL[entry.kind] ? i18n[INVITE_KIND_LABEL[entry.kind]] : entry.kind),
|
||||
td(a({ href: entry.href }, entry.title || entry.href)),
|
||||
td(moment(entry.at).format('YYYY/MM/DD HH:mm'))
|
||||
))
|
||||
)
|
||||
: p(i18n.invitesNoUnreachablePubs)
|
||||
: p(i18n.invitesUsedEmpty)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-houses', id: 'invites-houses' },
|
||||
h2(i18n.invitesHousesTitle || 'Houses'),
|
||||
p(i18n.invitesHousesHint),
|
||||
form(
|
||||
{ action: '/larp/invite/redeem', method: 'post' },
|
||||
input({ type: 'hidden', name: 'returnTo', value: '/invites' }),
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesHouseInviteCodePlaceholder || 'House invitation code', required: true, maxlength: 32 }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesHouseJoinButton || 'Join House')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-tribes', id: 'invites-tribes' },
|
||||
h2(i18n.invitesTribesTitle),
|
||||
p(i18n.invitesTribesHint),
|
||||
form(
|
||||
{ action: '/tribes/join-code', method: 'post' },
|
||||
input({ name: 'inviteCode', type: 'text', placeholder: i18n.invitesTribeInviteCodePlaceholder, required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesTribeJoinButton)
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-chats', id: 'invites-chats' },
|
||||
h2(i18n.invitesChatsTitle || 'Chats'),
|
||||
p(i18n.invitesChatsHint),
|
||||
form(
|
||||
{ action: '/chats/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesChatInviteCodePlaceholder || 'Enter chat invite code', required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesChatJoinButton || 'Join Chat')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-pads', id: 'invites-pads' },
|
||||
h2(i18n.invitesPadsTitle || 'Pads'),
|
||||
p(i18n.invitesPadsHint),
|
||||
form(
|
||||
{ action: '/pads/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesPadInviteCodePlaceholder || 'Enter pad invite code', required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesPadJoinButton || 'Join Pad')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-calendars', id: 'invites-calendars' },
|
||||
h2(i18n.invitesCalendarsTitle || 'Calendars'),
|
||||
p(i18n.invitesCalendarsHint),
|
||||
form(
|
||||
{ action: '/calendars/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesCalendarInviteCodePlaceholder || 'Enter calendar invite code', required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesCalendarJoinButton || 'Join Calendar')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-events', id: 'invites-events' },
|
||||
h2(i18n.invitesEventsTitle || 'Events'),
|
||||
p(i18n.invitesEventsHint),
|
||||
form(
|
||||
{ action: '/events/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesEventInviteCodePlaceholder || 'Enter event invite code', required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesEventJoinButton || 'Join Event')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-forums', id: 'invites-forums' },
|
||||
h2(i18n.invitesForumsTitle || 'Forums'),
|
||||
p(i18n.invitesForumsHint),
|
||||
form(
|
||||
{ action: '/forum/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesForumInviteCodePlaceholder || 'Enter forum invite code', required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesForumJoinButton || 'Join Forum')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-maps', id: 'invites-maps' },
|
||||
h2(i18n.invitesMapsTitle || 'Maps'),
|
||||
p(i18n.invitesMapsHint),
|
||||
form(
|
||||
{ action: '/maps/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesMapInviteCodePlaceholder || 'Enter map invite code', required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesMapJoinButton || 'Join Map')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-shops', id: 'invites-shops' },
|
||||
h2(i18n.invitesShopsTitle || 'Shops'),
|
||||
p(i18n.invitesShopsHint),
|
||||
form(
|
||||
{ action: '/shops/join-code', method: 'post' },
|
||||
input({ name: 'code', type: 'text', placeholder: i18n.invitesShopInviteCodePlaceholder || 'Enter shop invite code', required: true }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesShopJoinButton || 'Join Shop')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-inhabitants', id: 'invites-inhabitants' },
|
||||
h2(i18n.invitesInhabitantsTitle || 'Inhabitants'),
|
||||
p(i18n.invitesInhabitantsHint),
|
||||
form(
|
||||
{ action: '/invites/inhabitant/follow', method: 'post' },
|
||||
input({ name: 'feedId', id: 'inh_oasis_id', type: 'text', placeholder: '@...=.ed25519', pattern: '@[A-Za-z0-9+/_\\-]{43}=\\.ed25519', required: true, maxlength: 56 }),
|
||||
br(),
|
||||
button({ type: 'submit' }, i18n.invitesInhabitantsFollow || 'Give Support')
|
||||
)
|
||||
)
|
||||
),
|
||||
section(
|
||||
div({ class: 'invites-peers', id: 'invites-peers' },
|
||||
) : null,
|
||||
filter === 'others' ? section(
|
||||
div(
|
||||
h2(i18n.peers || 'Peers'),
|
||||
p(i18n.directConnectDescription),
|
||||
form({ action: '/peers/connect', method: 'post' },
|
||||
input({ type: 'text', id: 'peer_host', name: 'host', required: true, placeholder: '192.168.1.100', pattern: '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*)', title: i18n.peerHostValidation || 'Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.example.com)', maxlength: 253 }),
|
||||
input({ type: 'text', id: 'peer_host', name: 'host', required: true, placeholder: '192.168.1.100', pattern: '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)*)', title: i18n.peerHostValidation || 'Valid IPv4 (e.g. 192.168.1.100) or hostname (e.g. pub.solarnethub.com)', maxlength: 253 }),
|
||||
br(),
|
||||
label({ for: 'peer_port' }, i18n.peerPort),
|
||||
br(),
|
||||
|
|
@ -318,9 +234,10 @@ const invitesView = ({ invitesEnabled, flash }) => {
|
|||
button({ type: 'submit' }, i18n.connectAndFollow)
|
||||
)
|
||||
)
|
||||
)
|
||||
) : null
|
||||
);
|
||||
};
|
||||
|
||||
exports.invitesView = invitesView;
|
||||
exports.renderSuggestedPub = renderSuggestedPub;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue