OASIS_MOBILE/src/views/wiki_view.js
oasis-bot 948b6460d9 upstream: integrar Oasis 1.3.3
Diferencia 1.3.1 -> 1.3.3 del codigo de la APK oficial aplicada a tres
bandas encima de la rama: karma en el perfil, salas con directo, fotos
selladas, visibilidad de menciones, telefono por stream en Android y
las traducciones nuevas. Sin dependencias nuevas.

OasisMobile.css sigue siendo el del fork; del oficial solo se traen al
final las reglas de las piezas nuevas (karma, directo a pantalla
completa, salas en vivo, graphos).
2026-10-19 00:30:25 +02:00

382 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, ul, li, img } = require("../server/node_modules/hyperaxe");
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, renderCardMetaRow, paged } = require("./main_views");
const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderLicenseChip, renderLicenseSelect, renderTribeOriginChip } = require("./clearnet_view");
const { renderStyledText, richTextarea } = require("../backend/renderStyledText");
const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model");
const moment = require("../server/node_modules/moment");
const { config } = require("../server/SSB_server.js");
const userId = config.keys.id;
const FILTERS = ["all", "recent", "mine", "linked"];
const pageHref = (idOrSlug, tribeId = null) =>
`/wiki/${encodeURIComponent(idOrSlug)}${tribeId ? `?tribeId=${encodeURIComponent(tribeId)}` : ""}`;
const listHref = (filter, q, tribeId = null) => {
const parts = [`filter=${encodeURIComponent(filter || "all")}`];
if (q) parts.push(`q=${encodeURIComponent(q)}`);
if (tribeId) parts.push(`tribeId=${encodeURIComponent(tribeId)}`);
return `/wiki?${parts.join("&")}`;
};
const renderWikiBody = (body, opts = {}) => {
const missing = new Set(opts.missing || []);
const tribeId = opts.tribeId || null;
const wikiLink = (target, slug, label) => {
const isMissing = missing.has(slug);
return a({
href: isMissing ? `/wiki?filter=create&title=${encodeURIComponent(target)}${tribeId ? `&tribeId=${encodeURIComponent(tribeId)}` : ""}` : pageHref(slug, tribeId),
class: isMissing ? "wiki-link wiki-link-missing" : "wiki-link",
title: isMissing ? (i18n.wikiMissingLink || "Page does not exist yet") : label
}, label);
};
const paragraphs = String(body || "").replace(/\r\n/g, "\n").split(/\n{2,}/);
return div({ class: "wiki-body" },
...paragraphs.filter(par => par.trim()).map(par =>
p({ class: "wiki-paragraph" }, ...renderStyledText(par, { wikiLink, zoomImages: true }))
)
);
};
const diffLines = (oldText, newText) => {
const a = String(oldText || "").replace(/\r\n/g, "\n").split("\n");
const b = String(newText || "").replace(/\r\n/g, "\n").split("\n");
const n = a.length, m = b.length;
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
for (let i = n - 1; i >= 0; i--) {
for (let j = m - 1; j >= 0; j--) {
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
}
}
const out = [];
let i = 0, j = 0;
while (i < n && j < m) {
if (a[i] === b[j]) { out.push({ type: "ctx", text: a[i] }); i++; j++; }
else if (lcs[i + 1][j] >= lcs[i][j + 1]) { out.push({ type: "del", text: a[i] }); i++; }
else { out.push({ type: "add", text: b[j] }); j++; }
}
while (i < n) { out.push({ type: "del", text: a[i++] }); }
while (j < m) { out.push({ type: "add", text: b[j++] }); }
return out;
};
const renderDiff = (oldText, newText) =>
div({ class: "wiki-diff" },
...diffLines(oldText, newText).map(l =>
div({ class: `wiki-diff-line wiki-diff-${l.type}` }, span({ class: "wiki-diff-sign" }, l.type === "add" ? "+" : l.type === "del" ? "−" : " "), span({ class: "wiki-diff-text" }, l.text || " "))
)
);
const reachChip = (page) =>
page.tribeId || page.encrypted
? null
: renderReachChip(!!page.clearnet, i18n, page.clearnet ? clearnetItemHref('wiki', page.title, page.id) : null);
const statusChip = (page) =>
renderStateChip(page.editPolicy === "author" ? "closed" : "open", "", page.editPolicy === "author" ? i18n.wikiStatusClosed : i18n.wikiStatusOpen);
const pageChips = (page, params = {}, { withStatus = true } = {}) => [
page.encrypted ? renderEncryptedChip(i18n) : null,
withStatus ? statusChip(page) : null,
page.versionCount > 1 ? renderStateChip("neutral", "", `${page.versionCount} ${i18n.wikiVersions || "versions"}`) : null,
page.isLinked ? renderStateChip("open", "", i18n.wikiLinkedChip || "Linked") : null
].filter(Boolean);
const renderTags = (tags) => (Array.isArray(tags) && tags.length)
? div({ class: "card-tags" }, ...tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`)))
: null;
const renderPageCard = (page, params = {}) => {
const origin = page.tribeOrigin || null;
const href = origin ? origin.href : pageHref(page.tribeId ? page.id : page.slug, page.tribeId);
return div({ class: "tribe-card wiki-card" },
div({ class: "card-header activity-card-header" },
span(),
renderContentActions(page.id, href, origin
? { author: page.author, reportTitle: page.title }
: { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: (params.spreadMap && params.spreadMap.get(page.id)) || null, deleteAction: !page.tribeId && String(page.author) === String(userId) ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined })
),
div({ class: "tribe-card-body" },
page.image ? a({ href }, img({ loading: 'lazy', class: "wiki-card-cover", src: `/blob/${encodeURIComponent(page.image)}`, alt: page.title || "" })) : null,
div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, page.title || "—"))),
div({ class: "card-chips-row" }, ...(origin ? [renderTribeOriginChip(origin)] : pageChips(page)), !origin && page.clearnet === true ? reachChip(page) : null, renderLicenseChip(page.license)),
origin && params.withBody && page.body ? renderWikiBody(page.body) : null
)
);
};
const renderFilters = (filter, q, params = {}, census = []) => {
const tribeId = params.tribeId || null;
const chipVisible = (mode) => {
if (mode === filter || mode === "all") return true;
if (mode === "mine") return census.some(p => String(p.author) === String(userId) || (p.versions || []).some(v => String(v.author) === String(userId)));
if (mode === "recent") return census.length > 0;
if (mode === "linked") return census.some(p => p.isLinked);
return true;
};
const labelOf = (mode) => String(i18n[`wikiFilter${mode.charAt(0).toUpperCase() + mode.slice(1)}`] || mode).toUpperCase();
return div({ class: "filters" },
form({ method: "GET", action: "/wiki", class: "ui-toolbar ui-toolbar--filters" },
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
...(params.emptyMod ? [] : [
...FILTERS.filter(chipVisible).map(mode => button({ type: "submit", name: "filter", value: mode, class: filter === mode ? "filter-btn active" : "filter-btn" }, labelOf(mode))),
...((census.length > 0 || filter === "changes") ? [button({ type: "submit", name: "filter", value: "changes", class: filter === "changes" ? "filter-btn active" : "filter-btn" }, String(i18n.wikiChanges || "Recent changes").toUpperCase())] : [])
]),
button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.wikiCreate)
)
);
};
const renderForm = (page, params = {}) => {
const tribeId = (page && page.tribeId) || params.tribeId || "";
const draft = params.draft || null;
const status = draft ? (String(draft.status || "OPEN").toUpperCase() === "CLOSED" ? "CLOSED" : "OPEN") : (page && page.editPolicy === "author" ? "CLOSED" : "OPEN");
const titleValue = draft ? draft.title : (page ? page.title : (params.title || ""));
const bodyValue = draft ? draft.body : (page ? page.body : "");
const tagsValue = draft ? draft.tags : (page ? page.tags.join(", ") : "");
const summaryValue = draft ? draft.summary : "";
const licenseValue = draft && draft.license ? draft.license : (page ? page.license : "");
const canLicense = !page || String(page.author) === String(userId);
const statusOptions = [
{ value: "OPEN", label: i18n.wikiStatusOpen },
{ value: "CLOSED", label: i18n.wikiStatusClosed }
];
return div({ class: "div-center audio-form" },
h2(page ? i18n.wikiUpdateSectionTitle : i18n.wikiCreateSectionTitle),
draft
? section({ class: "post-preview wiki-preview" },
div({ class: "preview-content" },
h2(i18n.messagePreview),
h3({ class: "wiki-page-title" }, titleValue || "—"),
renderWikiBody(bodyValue, { missing: [], tribeId: tribeId || null })
)
)
: null,
form({ method: "POST", action: page ? `/wiki/update/${encodeURIComponent(page.id)}` : "/wiki/create", enctype: "multipart/form-data" },
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
page ? input({ type: "hidden", name: "pageId", value: page.id }) : null,
params.returnTo ? input({ type: "hidden", name: "returnTo", value: params.returnTo }) : null,
label(i18n.wikiTitleLabel), br(),
input({ type: "text", name: "title", maxlength: "100", required: true, placeholder: i18n.wikiTitlePlaceholder, value: titleValue }), br(),
label(i18n.wikiBodyLabel), br(),
richTextarea({ name: "body", rows: 14, maxlength: "20000", placeholder: i18n.wikiBodyPlaceholder }, bodyValue), br(),
label(i18n.uploadMedia), br(),
input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(),
page ? [label(i18n.wikiSummaryLabel), br(), input({ type: "text", name: "summary", maxlength: "200", placeholder: i18n.wikiSummaryPlaceholder, value: summaryValue }), br()] : null,
page
? null
: [label(i18n.wikiStatusLabel), br(), select({ name: "status" }, ...statusOptions.map(o => option({ value: o.value, ...(o.value === status ? { selected: true } : {}) }, o.label))), br(), br()],
label(i18n.wikiTagsLabel), br(),
input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.wikiTagsPlaceholder, value: tagsValue }), br(),
canLicense ? renderLicenseSelect(licenseValue, i18n) : null,
!tribeId && canLicense ? renderClearnetSelector(draft ? draft.clearnet === true : !!(page && page.clearnet), i18n) : null,
br(),
button({ type: "submit", class: "filter-btn", formaction: "/wiki/preview", attrs: { formmethod: "POST" } }, i18n.wikiPreview),
" ",
button({ type: "submit", class: "create-button" }, page ? i18n.wikiUpdate : i18n.wikiCreate)
)
);
};
const renderHeader = (tribe = null) =>
div({ class: "tags-header module-header-line" },
h2(tribe ? `${i18n.wikiTitle} · ${tribe.title}` : i18n.wikiTitle),
p(i18n.wikiDescription)
);
exports.wikiView = async (pages, filter = "all", params = {}) => {
const list = Array.isArray(pages) ? pages : [];
const q = String(params.q || "").trim();
const census = Array.isArray(params.censusList) ? params.censusList : list;
const isForm = filter === "create" || filter === "edit";
const emptyMod = !isForm && census.length === 0 && !q;
return template(
i18n.wikiTitle,
section(
div({ class: "tags-header module-header-line" },
h2(params.tribe ? `${i18n.wikiTitle} · ${params.tribe.title}` : i18n.wikiTitle),
p(i18n.wikiDescription)
),
renderFilters(isForm ? "all" : filter, q, { ...params, emptyMod }, census)
),
section(
isForm
? renderForm(filter === "edit" ? params.page : null, params)
: [
emptyMod ? null : div({ class: "filters activity-filter-chips activity-toolbar-row" },
renderModuleStats(list.length),
form({ method: "GET", action: "/wiki", class: "filter-box" },
input({ type: "hidden", name: "filter", value: filter }),
params.tribeId ? input({ type: "hidden", name: "tribeId", value: params.tribeId }) : null,
input({ type: "text", name: "q", value: q, placeholder: i18n.wikiSearchPlaceholder, class: "filter-box__input" }),
div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton))
)
),
div({ class: "tribe-grid" },
list.length ? paged(list).map(pg => renderPageCard(pg, params)) : p(i18n.wikiNoPages)
)
]
)
);
};
exports.wikiChangesView = async (changes, params = {}) => {
const list = Array.isArray(changes) ? changes : [];
const census = Array.isArray(params.censusList) ? params.censusList : [];
return template(
i18n.wikiTitle,
section(renderHeader(params.tribe || null), renderFilters("changes", "", { ...params, emptyMod: census.length === 0 }, census)),
section(
div({ class: "tags-header" }, h2(i18n.wikiChanges)),
list.length
? div({ class: "wiki-changes" }, ...paged(list).map(c =>
div({ class: "card-section wiki-change" },
div({ class: "card-field" },
span({ class: "card-label" }, `#${c.index}`),
span({ class: "card-value" }, a({ href: `${pageHref(c.pageId, c.tribeId)}${c.tribeId ? "&" : "?"}version=${encodeURIComponent(c.versionKey)}` }, c.title || c.slug))
),
c.summary ? p({ class: "wiki-change-summary" }, c.summary.startsWith("restore:") ? i18n.wikiRestoredSummary : c.summary) : null,
p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(c.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(c.author))
)
))
: div({ class: "no-content-box" }, p({ class: "no-content" }, i18n.wikiNoChanges))
)
);
};
const versionRow = (page, v, idx, current, tribeId) => {
const isCurrent = v.key === page.tipId;
const base = pageHref(page.tribeId ? page.id : page.slug, tribeId);
const sep = tribeId ? "&" : "?";
return div({ class: "card-section wiki-version" + (isCurrent ? " wiki-version-current" : "") },
div({ class: "card-field" },
span({ class: "card-label" }, `${i18n.wikiVersion} ${idx + 1}`),
isCurrent ? span({ class: "card-value" }, renderStateChip("mutuals", "", i18n.wikiCurrentVersion)) : span({ class: "card-value" })
),
v.summary ? [br(), p({ class: "wiki-change-summary" }, v.summary.startsWith("restore:") ? i18n.wikiRestoredSummary : v.summary), br()] : null,
div({ class: "tribe-side-actions wiki-version-actions" },
form({ method: "GET", action: base },
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
input({ type: "hidden", name: "version", value: v.key }),
button({ type: "submit", class: "update-btn" }, i18n.wikiViewVersion)),
idx > 0
? form({ method: "GET", action: base },
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
input({ type: "hidden", name: "diff", value: v.key }),
button({ type: "submit", class: "update-btn" }, i18n.wikiDiff))
: null,
!isCurrent && page.canEdit
? form({ method: "POST", action: `/wiki/restore/${encodeURIComponent(page.id)}` },
input({ type: "hidden", name: "version", value: v.key }),
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
button({ type: "submit", class: "update-btn" }, i18n.wikiRestore))
: null
),
p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(v.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(v.author))
);
};
exports.wikiHistoryView = async (page, params = {}) => {
const tribeId = page.tribeId || null;
const versions = page.versions.slice().reverse();
const census = Array.isArray(params.censusList) ? params.censusList : [];
return template(
page.title,
section(renderHeader(params.tribe || null), renderFilters("all", "", { ...params, emptyMod: census.length === 0 }, census)),
section(
div({ class: "tags-header" }, h2(i18n.wikiHistory)),
div({ class: "wiki-history" }, ...versions.map((v, i) => versionRow(page, v, page.versions.length - 1 - i, page.tipId, tribeId)))
)
);
};
exports.wikiPageView = async (page, params = {}) => {
const tribeId = page.tribeId || null;
const base = pageHref(tribeId ? page.id : page.slug, tribeId);
const sep = tribeId ? "&" : "?";
const version = params.version || null;
const diffVersion = params.diff || null;
const shownTitle = version ? version.title : page.title;
const shownBody = version ? version.body : page.body;
const census = Array.isArray(params.censusList) ? params.censusList : [];
const subscriptionNodes = params.subscription
? renderSubscriptionBox({ target: page.id, scope: "wiki", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: page.isOwner, returnTo: base, inline: true, compact: true })
: null;
const barChips = [reachChip(page), statusChip(page)].filter(Boolean);
const actions = div({ class: "tribe-side-actions wiki-actions-top" },
span({ class: "wiki-actions-left" }, ...barChips),
page.isOwner && !tribeId && !page.encrypted ? renderClearnetSwitch("wiki", page.rootId || page.id, !!page.clearnet) : null,
...(subscriptionNodes && subscriptionNodes.length ? subscriptionNodes : []),
page.canEdit && !version
? form({ method: "GET", action: "/wiki" },
input({ type: "hidden", name: "filter", value: "edit" }),
input({ type: "hidden", name: "id", value: page.id }),
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
button({ type: "submit", class: "update-btn" }, i18n.wikiEdit))
: null,
form({ method: "GET", action: `/wiki/${encodeURIComponent(page.id)}/history` },
tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null,
button({ type: "submit", class: "update-btn" }, `${i18n.wikiHistory} (${page.versionCount})`)),
form({ method: "GET", action: `/wiki/${encodeURIComponent(page.id)}/pdf` },
button({ type: "submit", class: "update-btn" }, i18n.generatePdf)),
page.isOwner && tribeId
? form({ method: "POST", action: `/wiki/delete/${encodeURIComponent(page.id)}` }, input({ type: "hidden", name: "tribeId", value: tribeId }), button({ type: "submit", class: "delete-btn" }, i18n.wikiDelete))
: null
);
let diffBlock = null;
if (diffVersion) {
const idx = page.versions.findIndex(v => v.key === diffVersion.key);
const prev = idx > 0 ? page.versions[idx - 1] : null;
diffBlock = div({ class: "card-section" },
h3(`${i18n.wikiDiff}: ${i18n.wikiVersion} ${idx} → ${idx + 1}`),
renderDiff(prev ? prev.body : "", diffVersion.body)
);
}
return template(
page.title,
section(renderHeader(params.tribe || null), renderFilters("all", "", { ...params, emptyMod: census.length === 0 }, census)),
section(
div({ class: "trending-card wiki-page" },
div({ class: "card-header activity-card-header" },
span(),
renderContentActions(page.id, base, { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: params.spread || null, deleteAction: page.isOwner && !tribeId ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined })
),
div({ class: "card-section" },
actions,
div({ class: "shop-title-row" }, h2({ class: "wiki-page-title" }, shownTitle), renderLicenseChip(page.license)),
(() => { const rest = pageChips(page, params, { withStatus: false }); return rest.length ? div({ class: "card-chips-row" }, ...rest) : null; })(),
version ? p({ class: "wiki-version-notice" }, `${i18n.wikiViewingVersion} (${moment(version.createdAt).format("YYYY/MM/DD HH:mm")}) — `, a({ href: base }, i18n.wikiBackToPage)) : null,
diffBlock || renderWikiBody(shownBody, { missing: page.missingLinks, tribeId }),
renderTags(page.tags),
renderCardMetaRow(p({ class: "card-footer" },
span({ class: "date-link" }, `${moment(page.createdAt).format("YYYY/MM/DD HH:mm")}`),
userLink(page.author)
))
),
),
div({ class: "card-section wiki-backlinks" },
h3(i18n.wikiLinkedPages),
page.linkedPages && page.linkedPages.length
? ul(...page.linkedPages.map(b => li(a({ href: pageHref(b.tribeId ? b.id : b.slug, b.tribeId) }, b.title))))
: p(i18n.wikiNoLinkedPages)
)
)
);
};
exports.renderTribeWikiSection = (tribe, pages, toolbar = null, wrap = (pg, card) => card) => {
const items = Array.isArray(pages) ? pages : [];
const createBtn = form({ method: "GET", action: "/wiki" },
input({ type: "hidden", name: "filter", value: "create" }),
input({ type: "hidden", name: "tribeId", value: tribe.id }),
button({ type: "submit", class: "create-button" }, i18n.tribeWikiCreate));
const header = toolbar || div({ class: "tribe-content-header" }, h2(i18n.tribeSectionWiki), createBtn);
if (!items.length) return div({ class: "tribe-content-list" }, header, div({ class: "no-content-box" }, p(i18n.tribeWikiEmpty)));
return div({ class: "tribe-content-list" },
header,
div({ class: "tribe-grid" }, ...paged(items).map(pg => wrap(pg, renderPageCard(pg, { tribeId: tribe.id, withBody: true }))))
);
};
exports.renderWikiBody = renderWikiBody;
exports.diffLines = diffLines;