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).
This commit is contained in:
parent
bc64565e9c
commit
948b6460d9
95 changed files with 3613 additions and 1356 deletions
|
|
@ -837,7 +837,11 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
|||
)
|
||||
: '',
|
||||
contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '',
|
||||
bodyNode
|
||||
bodyNode,
|
||||
Array.isArray(content?.tags) && content.tags.filter(Boolean).length
|
||||
? div({ class: 'card-tags' }, content.tags.filter(Boolean).map(tag =>
|
||||
a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
|
||||
: ''
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -1883,6 +1887,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e
|
|||
),
|
||||
renderContentActions(msgId, detailHref, {
|
||||
author: action.author,
|
||||
contact: type === 'about',
|
||||
reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '',
|
||||
spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined,
|
||||
...(type === 'torrent' || type === 'file' ? { spreadTitle: i18n.seedAction } : {}),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const {
|
|||
const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view");
|
||||
|
||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderStyledText } = require("../backend/renderStyledText")
|
||||
|
|
@ -61,7 +61,7 @@ const renderAudioPlayer = (audioObj, opts = {}) =>
|
|||
audioObj?.url
|
||||
? div(
|
||||
{ class: "audio-container" },
|
||||
audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(audioObj.url)}`, preload: "metadata" }),
|
||||
audioHyperaxe({ controls: true, src: blobUrlOf(audioObj.url), preload: "metadata" }),
|
||||
opts.skipTranscode ? null : renderTranscodeButton(audioObj)
|
||||
)
|
||||
: p(i18n.audioNoFile);
|
||||
|
|
@ -236,7 +236,6 @@ exports.audioView = async (audios, filter = "all", audioId = null, params = {})
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
||||
|
|
@ -380,7 +379,6 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/audios", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterAll).toUpperCase()),
|
||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.audioFilterRecent).toUpperCase())] : []),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br, strong, label, ul, li } = require("../server/node_modules/hyperaxe");
|
||||
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
||||
const { template, i18n, userLink, formatCarbon, renderStateChip, renderModuleStats, renderWalletChip, paged } = require("../views/main_views");
|
||||
const { template, i18n, userLink, formatCarbon, renderStateChip, renderModuleStats, renderWalletChip, paged, renderIcon } = require("../views/main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
|
||||
const FILTER_LABELS = {
|
||||
|
|
@ -489,7 +489,7 @@ const renderUbiPubs = (pubs, hasValidWallet) => {
|
|||
userLink(p.pubId),
|
||||
p.isThisNode || p.isConnected ? span({ class: "status you" }, p.isThisNode ? i18n.bankUbiPubThisNode : i18n.bankUbiPubConnected) : null,
|
||||
hasValidWallet && p.address
|
||||
? div({ class: "content-actions" }, form({ method: "GET", action: "/banking/fund", class: "content-action-form" }, input({ type: "hidden", name: "pub", value: p.pubId }), button({ type: "submit", class: "btn-singleview btn-donate", title: i18n.bankDonateEco }, "\u2744")))
|
||||
? div({ class: "content-actions" }, form({ method: "GET", action: "/banking/fund", class: "content-action-form" }, input({ type: "hidden", name: "pub", value: p.pubId }), button({ type: "submit", class: "btn-singleview btn-donate", title: i18n.bankDonateEco }, renderIcon("donate"))))
|
||||
: null
|
||||
),
|
||||
div({ class: "bank-summary ubi-pub-summary" },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { div, h2, h3, p, section, button, form, a, input, span, pre, table, tr, td, strong } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, paged } = require("../views/main_views");
|
||||
const { template, i18n, userLink, paged, renderIcon } = require("../views/main_views");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
|
||||
const FILTER_LABELS = {
|
||||
|
|
@ -175,7 +175,9 @@ const getViewDetailsAction = (type, block) => {
|
|||
case 'event': return `/events/${encodeURIComponent(block.id)}`;
|
||||
case 'task': return `/tasks/${encodeURIComponent(block.id)}`;
|
||||
case 'about': return `/author/${encodeURIComponent(block.author)}`;
|
||||
case 'post': return `/thread/${encodeURIComponent(block.id)}#${encodeURIComponent(block.id)}`;
|
||||
case 'post': return block.content && typeof block.content.root === 'string'
|
||||
? `/thread/${encodeURIComponent(block.content.root)}#${encodeURIComponent(block.id)}`
|
||||
: `/blogs/${encodeURIComponent(block.id)}`;
|
||||
case 'vote': return `/thread/${encodeURIComponent(block.content.vote.link)}#${encodeURIComponent(block.content.vote.link)}`;
|
||||
case 'contact': return `/inhabitants`;
|
||||
case 'pub': return `/invites`;
|
||||
|
|
@ -424,7 +426,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi
|
|||
button({ type:'submit', class:'filter-btn' }, `← ${i18n.blockchainBack}`)
|
||||
),
|
||||
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open'))
|
||||
: (block.isTombstoned || block.isReplaced) ?
|
||||
div({ class: 'deleted-label' },
|
||||
i18n.blockchainContentDeleted || "This content has been deleted."
|
||||
|
|
@ -534,10 +536,10 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {})
|
|||
.map(block=>
|
||||
div({ class:'block' },
|
||||
div({ class:'block-buttons' },
|
||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, '⦿'),
|
||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, '⊞'),
|
||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}`, class:'btn-singleview', title:i18n.blockchainDetails }, renderIcon('block')),
|
||||
block.restricted ? null : a({ href:`/blockexplorer/block/${encodeURIComponent(block.id)}${qs}&view=datagram`, class:'btn-singleview btn-datagram', title:i18n.blockchainDatagram }, renderIcon('datagram')),
|
||||
!block.isTombstoned && !block.isReplaced && getViewDetailsAction(block.type, block) ?
|
||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, '↗')
|
||||
a({ href:getViewDetailsAction(block.type, block), class:'btn-singleview btn-content', title:i18n.visitContent }, renderIcon('open'))
|
||||
: (block.isTombstoned || block.isReplaced) ?
|
||||
div({ class: 'deleted-label' },
|
||||
i18n.blockchainContentDeleted || "This content has been deleted."
|
||||
|
|
|
|||
|
|
@ -21,9 +21,16 @@ const FILTERS = [
|
|||
|
||||
const safeText = (v) => String(v == null ? "" : v).trim();
|
||||
|
||||
const renderBlogTags = (tags) => {
|
||||
const list = (Array.isArray(tags) ? tags : []).map(t => String(t || "").trim().replace(/^#+/, "")).filter(Boolean);
|
||||
return list.length
|
||||
? div({ class: "card-tags" }, list.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
|
||||
: null;
|
||||
};
|
||||
|
||||
const excerpt = (text, max = 420) => {
|
||||
const s = safeText(text);
|
||||
return s.length > max ? s.slice(0, max) + "…" : s;
|
||||
return s.length > max ? s.slice(0, max).replace(/\s+$/, "") + "…" : s;
|
||||
};
|
||||
|
||||
const renderFilterBar = (filter, q, showSearch = true, total = null, censusList = null) => {
|
||||
|
|
@ -77,6 +84,7 @@ const renderBlogCard = (blog, filter, spreadInfo) => {
|
|||
: null,
|
||||
blog.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("blog", blog.subject || blog.text, blog.id))) : null,
|
||||
div({ class: "blog-card-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(excerpt(blog.text))) }),
|
||||
renderBlogTags(blog.tags),
|
||||
p({ class: "card-footer" },
|
||||
span({ class: "date-link" }, `${moment(blog.createdAt).format("YYYY/MM/DD HH:mm")}`),
|
||||
userLink(blog.author)
|
||||
|
|
@ -89,13 +97,15 @@ const renderCreateForm = (draft = null) => {
|
|||
const subjectValue = draft ? safeText(draft.subject) : "";
|
||||
const textValue = draft ? String(draft.text || "") : "";
|
||||
const allowComments = draft ? draft.allowComments !== false : true;
|
||||
const tagsValue = draft ? (Array.isArray(draft.tags) ? draft.tags.join(", ") : safeText(draft.tags)) : "";
|
||||
return section(
|
||||
draft
|
||||
? section({ class: "post-preview blog-preview" },
|
||||
div({ class: "preview-content" },
|
||||
h2(i18n.messagePreview),
|
||||
subjectValue ? h2({ class: "tribe-card-title" }, subjectValue) : null,
|
||||
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(textValue)) })
|
||||
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(textValue)) }),
|
||||
renderBlogTags(draft.tags)
|
||||
)
|
||||
)
|
||||
: null,
|
||||
|
|
@ -105,17 +115,19 @@ const renderCreateForm = (draft = null) => {
|
|||
br(),
|
||||
input({ type: "text", name: "subject", id: "subject", maxlength: "150", placeholder: i18n.contentWarningPlaceholder, value: subjectValue }),
|
||||
br(),
|
||||
br(),
|
||||
label({ for: "text" }, i18n.blogMessage),
|
||||
br(),
|
||||
textarea({ required: true, name: "text", id: "text", rows: "8", class: "publish-textarea", maxlength: String(TEXT_CAP), placeholder: i18n.publishWarningPlaceholder }, textValue),
|
||||
br(),
|
||||
br(),
|
||||
label({ for: "blob" }, i18n.blogMedia),
|
||||
br(),
|
||||
input({ type: "file", id: "blob", name: "blob", multiple: true, accept: "image/*,video/*,audio/*,application/pdf" }),
|
||||
br(),
|
||||
br(),
|
||||
label({ for: "tags" }, i18n.tagsLabel || "Tags"),
|
||||
br(),
|
||||
input({ type: "text", name: "tags", id: "tags", placeholder: i18n.tagsPlaceholder || "", value: tagsValue }),
|
||||
br(),
|
||||
div({ class: "blog-allow-comments" },
|
||||
input({ type: "hidden", name: "allowComments", value: "0" }),
|
||||
label(
|
||||
|
|
@ -206,7 +218,8 @@ exports.singleBlogView = async (blog, comments = [], params = {}) => {
|
|||
|
||||
const blogMain = div({ class: "tribe-main" },
|
||||
div({ class: "job-section" },
|
||||
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(blog.text || "")) })
|
||||
div({ class: "blog-detail-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(blog.text || "")) }),
|
||||
renderBlogTags(blog.tags)
|
||||
),
|
||||
renderEngagement(blog.id,
|
||||
renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants),
|
||||
|
|
|
|||
|
|
@ -227,7 +227,6 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()),
|
||||
|
|
@ -369,7 +368,6 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/bookmarks", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()),
|
||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase())] : []),
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
|||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/campaigns", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
...(emptyMod ? [] : [
|
||||
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
||||
...chipButtons(STATUSES, filter, census, statusLabel),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const catLabel = (c) => i18n[catKey(c)] || c
|
|||
|
||||
const blobSrcOf = (value) => {
|
||||
const s = String(value || "").trim()
|
||||
if (s.startsWith("/blob/sealed/")) return s
|
||||
if (s.startsWith("&")) return `/blob/${encodeURIComponent(s)}`
|
||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
||||
return mImg ? `/blob/${encodeURIComponent(mImg[1])}` : null
|
||||
|
|
@ -30,9 +31,10 @@ const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => {
|
|||
if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||
const s = String(value).trim()
|
||||
if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||
if (s.startsWith("/blob/sealed/")) return img({ loading: 'lazy', src: s, ...attrs })
|
||||
if (s.startsWith("&")) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs })
|
||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
||||
if (mImg) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs })
|
||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||
if (mImg) return img({ loading: 'lazy', src: mImg[1].startsWith("/blob/sealed/") ? mImg[1] : `/blob/${encodeURIComponent(mImg[1])}`, ...attrs })
|
||||
return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,47 @@ const renderClearnetUrlBlock = ({ baseUrl = '', path, i18nObj = {} }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const CLEARNET_ZOOM_CSS = `
|
||||
.cn-zoom{display:contents}
|
||||
.cn-zoom img{cursor:zoom-in}
|
||||
.cn-lightbox{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.9);align-items:center;justify-content:center}
|
||||
.cn-lightbox:target{display:flex}
|
||||
.cn-lightbox-link{display:flex;align-items:center;justify-content:center;width:100%;height:100%;cursor:zoom-out}
|
||||
.cn-lightbox-img{display:block;max-width:92vw;max-height:88vh;object-fit:contain}
|
||||
.cn-lightbox-full{background:#000}
|
||||
.cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh;max-width:100vw;max-height:100vh}
|
||||
.cn-lightbox-close,.cn-lightbox-fs{position:absolute;top:14px;z-index:1;width:42px;height:42px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(0,0,0,.6);color:#fff;text-decoration:none;font-size:24px;line-height:1}
|
||||
.cn-lightbox-close{right:16px}
|
||||
.cn-lightbox-fs{right:66px}
|
||||
.cn-lightbox-fs svg{display:block;width:20px;height:20px}
|
||||
`;
|
||||
const CN_FULLSCREEN_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 9 4 4 9 4"/><polyline points="15 4 20 4 20 9"/><polyline points="20 15 20 20 15 20"/><polyline points="9 20 4 20 4 15"/></svg>';
|
||||
const cnZoom = (id, src, alt = '', { full = false, imgClass = '' } = {}) => {
|
||||
const zid = `cnz-${String(id).replace(/[^A-Za-z0-9_-]/g, '')}`;
|
||||
const a = escapeHtml(alt);
|
||||
const u = escapeHtml(src);
|
||||
const cls = imgClass ? ` class="${escapeHtml(imgClass)}"` : '';
|
||||
const fs = full ? `<a class="cn-lightbox-fs" href="#${zid}-full" title="${escapeHtml(cnText('liveFullscreen', 'Full screen'))}">${CN_FULLSCREEN_ICON}</a>` : '';
|
||||
return `<a class="cn-zoom" id="${zid}-src" href="#${zid}"><img${cls} src="${u}" alt="${a}"/></a>`
|
||||
+ `<div class="cn-lightbox" id="${zid}"><a class="cn-lightbox-close" href="#${zid}-src">×</a>${fs}<a class="cn-lightbox-link" href="#${zid}-src"><img class="cn-lightbox-img" src="${u}" alt="${a}" loading="lazy"/></a></div>`
|
||||
+ (full ? `<div class="cn-lightbox cn-lightbox-full" id="${zid}-full"><a class="cn-lightbox-close" href="#${zid}">×</a><a class="cn-lightbox-link" href="#${zid}"><img class="cn-lightbox-img" src="${u}" alt="${a}" loading="lazy"/></a></div>` : '');
|
||||
};
|
||||
const CN_ZOOM_SKIP = /\bcn-(lightbox-img|footer-logo)\b/;
|
||||
const zoomClearnetImages = (html) => {
|
||||
let depth = 0;
|
||||
let n = 0;
|
||||
return String(html || '').replace(/<a\b[^>]*>|<\/a\s*>|<img\b[^>]*>/gi, (tag) => {
|
||||
if (/^<a\b/i.test(tag)) { depth++; return tag; }
|
||||
if (/^<\/a/i.test(tag)) { depth = Math.max(0, depth - 1); return tag; }
|
||||
if (depth > 0) return tag;
|
||||
const cls = (tag.match(/\sclass="([^"]*)"/i) || [])[1] || '';
|
||||
const src = (tag.match(/\ssrc="([^"]*)"/i) || [])[1] || '';
|
||||
if (!src || CN_ZOOM_SKIP.test(cls)) return tag;
|
||||
const alt = (tag.match(/\salt="([^"]*)"/i) || [])[1] || '';
|
||||
const zid = `cnz-i${++n}`;
|
||||
return `<a class="cn-zoom" id="${zid}-src" href="#${zid}">${tag}</a><span class="cn-lightbox" id="${zid}"><a class="cn-lightbox-close" href="#${zid}-src">×</a><a class="cn-lightbox-link" href="#${zid}-src"><img class="cn-lightbox-img" src="${src}" alt="${alt}" loading="lazy"/></a></span>`;
|
||||
});
|
||||
};
|
||||
const CLEARNET_TEXT_CSS = `
|
||||
.cn-tags{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
|
||||
.cn-hub-priceline{margin:10px 0 0 0}
|
||||
|
|
@ -266,8 +307,11 @@ a.tag-link:hover{text-decoration:underline}
|
|||
.rt-rule{display:block;border-top:1px solid currentColor;opacity:.4;margin:10px 0}
|
||||
.rt-code{font-family:monospace;background:rgba(128,128,128,.18);padding:1px 4px;border-radius:3px;word-break:break-word}
|
||||
.rt-code-block{display:block;font-family:monospace;white-space:pre-wrap;background:rgba(128,128,128,.18);padding:10px;border-radius:5px;margin:8px 0;overflow-x:auto;word-break:break-word}
|
||||
.post-image{display:block;max-width:100%;height:auto;margin:12px 0;border:1px solid var(--border);border-radius:6px}
|
||||
.post-video,.post-audio{display:block;width:100%;max-width:100%;margin:12px 0;border-radius:6px;background:#000}
|
||||
.post-image{display:block;width:auto;max-width:100%;max-height:480px;height:auto;margin:12px auto;border:1px solid var(--border);border-radius:6px}
|
||||
.post-video{display:block;width:100%;max-width:100%;max-height:480px;margin:12px auto;border-radius:6px;background:#000}
|
||||
.post-audio{display:block;width:100%;max-width:100%;margin:12px 0}
|
||||
.cn-media-frame img{max-height:480px;width:auto;margin:0 auto}
|
||||
.cn-media-frame video{max-height:480px}
|
||||
.post-pdf{display:inline-block;margin:8px 0;padding:8px 14px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);text-decoration:none}
|
||||
.post-pdf:hover{border-color:var(--fg)}
|
||||
`;
|
||||
|
|
@ -346,7 +390,10 @@ footer.cn-footer .cn-footer-logo{width:56px;height:auto;display:block;margin:0 a
|
|||
footer.cn-footer .cn-footer-line{margin:3px 0}
|
||||
`;
|
||||
|
||||
const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = '', hubFeedId = null }) => {
|
||||
const renderClearnetHubSearch = ({ type = '', query = '' } = {}) =>
|
||||
`<form class="cn-search" method="GET" action="/c">${type ? `<input type="hidden" name="type" value="${escapeHtml(type)}"/>` : ''}<input type="text" name="q" value="${escapeHtml(query || '')}" placeholder="${escapeHtml(cnText('cnSearchPlaceholder', 'Search…'))}" autocomplete="off"/></form>`;
|
||||
|
||||
const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = null, hubFeedId = null }) => {
|
||||
const safeTitle = escapeHtml(title || 'Oasis');
|
||||
const safeOgTitle = escapeHtml(ogTitle || title || 'Oasis');
|
||||
const safeOgDesc = escapeHtml(ogDescription || '');
|
||||
|
|
@ -368,7 +415,7 @@ const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null
|
|||
<meta name="description" content="${safeOgDesc}"/>
|
||||
<meta name="robots" content="index, follow"/>
|
||||
<link rel="icon" href="/c/assets/images/favicon.svg"/>
|
||||
<style>${baseCss}${CLEARNET_SEARCH_CSS}${CLEARNET_TEXT_CSS}${extraCss}
|
||||
<style>${baseCss}${CLEARNET_SEARCH_CSS}${CLEARNET_TEXT_CSS}${CLEARNET_ZOOM_CSS}${extraCss}
|
||||
.cn-brand-link{display:block;text-decoration:none}
|
||||
|
||||
.cn-brand-link:hover .cn-brand{color:var(--accent)}
|
||||
|
|
@ -386,9 +433,9 @@ const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null
|
|||
<body>
|
||||
<header class="cn-header">
|
||||
${brandBlock}
|
||||
<div class="cn-header-extra">${renderLangSelector(lang)}${propagateLang(headerExtra || '', langOverride)}</div>
|
||||
<div class="cn-header-extra">${renderLangSelector(lang)}${propagateLang(headerExtra == null ? renderClearnetHubSearch() : headerExtra, langOverride)}</div>
|
||||
</header>
|
||||
${propagateLang(stripInternalAnchors(body), langOverride)}
|
||||
${propagateLang(stripInternalAnchors(zoomClearnetImages(body)), langOverride)}
|
||||
<footer class="cn-footer">
|
||||
<a href="https://wiki.solarnethub.com" target="_blank" rel="noopener"><img class="cn-footer-logo" src="/c/assets/images/snh-oasis.jpg" alt="Oasis"/></a>
|
||||
<div class="cn-footer-line">${escapeHtml(cnText('cnSyncedPeers', 'Synced-peers'))}: [ <strong>${Number(sharedState.getSyncedPeerCount ? sharedState.getSyncedPeerCount() : 0) || 0}</strong> ]</div>
|
||||
|
|
@ -550,6 +597,7 @@ const renderClearnetPodcastView = ({ channel }) => {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
cnZoom,
|
||||
kindLabel,
|
||||
renderTagChips,
|
||||
renderClearnetPodcastView,
|
||||
|
|
@ -575,6 +623,7 @@ module.exports = {
|
|||
renderDoubleEncryptionChip,
|
||||
renderClearnetUrlBlock,
|
||||
renderClearnetSearchForm,
|
||||
renderClearnetHubSearch,
|
||||
renderClearnetPage,
|
||||
renderClearnetNotFound,
|
||||
CLEARNET_PAGER_CSS,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const { div, p, h2, span, a, form, input, textarea, button, label, br, details, summary } = require("../server/node_modules/hyperaxe");
|
||||
const { TEXT_CAP } = require('../backend/long_text');
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { i18n, userLink } = require("./main_views");
|
||||
const { i18n, userLink, renderHideIcon, renderIcon } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderStyledText } = require("../backend/renderStyledText");
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ const visibleComments = (comments) => (Array.isArray(comments) ? comments : []).
|
|||
return t && String(t).trim();
|
||||
});
|
||||
|
||||
const renderCommentCard = (c, extra = null) => {
|
||||
const renderCommentCard = (c, extra = null, returnTo = null) => {
|
||||
const author = (c && c.value && c.value.author) || "";
|
||||
const ts = (c && c.value && c.value.timestamp) || (c && c.timestamp);
|
||||
const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : "";
|
||||
|
|
@ -28,7 +28,13 @@ const renderCommentCard = (c, extra = null) => {
|
|||
return div({ class: own ? "votations-comment-card own-comment" : "votations-comment-card", id: c && c.key ? c.key : undefined },
|
||||
own && c.key
|
||||
? form({ method: "POST", action: `/comments/delete/${encodeURIComponent(c.key)}`, class: "comment-delete-form" },
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.chatDelete }, "✕"))
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.chatDelete }, renderIcon("close")))
|
||||
: null,
|
||||
!own && c.key
|
||||
? form({ method: "POST", action: `/hide/${encodeURIComponent(c.key)}`, class: "comment-delete-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo || (rootId ? `/thread/${encodeURIComponent(rootId)}` : "/activity") }),
|
||||
input({ type: "hidden", name: "label", value: String(text).replace(/\s+/g, " ").trim().slice(0, 80) }),
|
||||
button({ type: "submit", class: "btn-singleview btn-hide", title: i18n.hideContent }, renderHideIcon()))
|
||||
: null,
|
||||
span({ class: "created-at" },
|
||||
author ? userLink(author) : span("(unknown)"),
|
||||
|
|
@ -52,7 +58,7 @@ const renderCommentsSection = ({ action, comments = [], returnTo = null, closedN
|
|||
div({ class: "vote-comments-section" },
|
||||
list.length
|
||||
? div({ class: "comments-list" }, ...list.map((c, i) => {
|
||||
const card = renderCommentCard(c, typeof commentExtra === "function" ? commentExtra(c) : null);
|
||||
const card = renderCommentCard(c, typeof commentExtra === "function" ? commentExtra(c) : null, returnTo);
|
||||
return i === list.length - 1 ? div({ id: "comments-latest", class: "comments-latest-anchor" }, card) : card;
|
||||
}))
|
||||
: (closedNote ? null : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet)),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { div, h2, p, section, button, form, a, input, span, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, renderModuleStats, paged } = require("./main_views");
|
||||
const { template, i18n, userLink, renderModuleStats, paged, renderIcon } = require("./main_views");
|
||||
|
||||
const KIND_LABEL = {
|
||||
inhabitants: () => i18n.dataKindInhabitants,
|
||||
|
|
@ -32,7 +32,8 @@ const KIND_LABEL = {
|
|||
mailing: () => i18n.mailingTitle,
|
||||
logistics: () => i18n.logisticsTitle,
|
||||
podcasts: () => i18n.podcastsTitle,
|
||||
campaigns: () => i18n.campaignsTitle
|
||||
campaigns: () => i18n.campaignsTitle,
|
||||
blogs: () => i18n.blogTitle
|
||||
};
|
||||
|
||||
const REASON_LABEL = {
|
||||
|
|
@ -54,39 +55,50 @@ const FILTER_COLUMNS = [
|
|||
["INHABITANTS", "TRIBES"],
|
||||
["VOTES", "EVENTS", "CALENDARS", "TASKS", "REPORTS", "EMERGENCIES", "MAILING", "CAMPAIGNS"],
|
||||
["MARKET", "HOUSING", "JOBS", "PROJECTS", "INDUSTRY", "LOGISTICS"],
|
||||
["FORUM", "CHATS", "ROOMS", "PADS", "WIKI", "MAPS", "SCHOOL"],
|
||||
["BLOGS", "FORUM", "CHATS", "ROOMS", "PADS", "WIKI", "MAPS", "SCHOOL"],
|
||||
["AUDIOS", "BOOKMARKS", "DOCUMENTS", "FILES", "IMAGES", "TORRENTS", "VIDEOS", "PODCASTS"]
|
||||
];
|
||||
|
||||
const kindLabel = (kind) => (KIND_LABEL[kind] ? KIND_LABEL[kind]() : String(kind || '').toUpperCase());
|
||||
|
||||
const reasonButton = (value, label, current, q) =>
|
||||
const reasonButton = (value, label, current, filter) =>
|
||||
form({ method: "GET", action: "/data" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
filter && filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null,
|
||||
value ? input({ type: "hidden", name: "reason", value }) : null,
|
||||
button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase())
|
||||
);
|
||||
|
||||
const filterButton = (value, label, current, reason) =>
|
||||
form({ method: "GET", action: "/data" },
|
||||
input({ type: "hidden", name: "filter", value }),
|
||||
reason ? input({ type: "hidden", name: "reason", value: reason }) : null,
|
||||
button({ type: "submit", class: current === value ? "filter-btn active" : "filter-btn" }, String(label).toUpperCase())
|
||||
);
|
||||
|
||||
const sectionId = (kind) => `data-${String(kind || '').toLowerCase()}`;
|
||||
|
||||
const renderFilters = (q, payload = {}, kindsPresent = []) => {
|
||||
const matches = Array.isArray(payload.matches) ? payload.matches : [];
|
||||
const reason = String(payload.reason || "");
|
||||
if (!matches.length && !String(q || "").trim() && !reason) return null;
|
||||
const filter = String(payload.filter || "ALL").toUpperCase();
|
||||
const isKindFilter = !FILTER_COLUMNS[0].includes(filter);
|
||||
if (!matches.length && !String(q || "").trim() && !reason && !isKindFilter) return null;
|
||||
const avail = (Array.isArray(payload.reasonsAvail) ? payload.reasonsAvail : []).filter(r => REASON_LABEL[r]);
|
||||
const present = new Set(kindsPresent);
|
||||
const columns = FILTER_COLUMNS.slice(1)
|
||||
.map(col => col.filter(k => present.has(k.toLowerCase())))
|
||||
.filter(col => col.length);
|
||||
const columns = [FILTER_COLUMNS[0]].concat(
|
||||
FILTER_COLUMNS.slice(1)
|
||||
.map(col => col.filter(k => present.has(k.toLowerCase())))
|
||||
.filter(col => col.length)
|
||||
);
|
||||
return section(
|
||||
div({ class: "activity-sub-filter" },
|
||||
reasonButton("", i18n.dataFilterAll, reason, q),
|
||||
...avail.map(r => reasonButton(r, REASON_LABEL[r](), reason, q))
|
||||
reasonButton("", i18n.dataFilterAll, reason, filter),
|
||||
...avail.map(r => reasonButton(r, REASON_LABEL[r](), reason, filter))
|
||||
),
|
||||
reason || !columns.length ? null : div({ class: "activity-filter-grid" },
|
||||
div({ class: "activity-filter-grid" },
|
||||
...columns.map(col =>
|
||||
div({ class: "activity-filter-col" },
|
||||
...col.map(k => a({ href: `#${sectionId(k)}`, class: "filter-btn" }, String(kindLabel(k.toLowerCase())).toUpperCase()))
|
||||
...col.map(k => filterButton(k, FILTER_COLUMNS[0].includes(k) ? (i18n[k + "Button"] || k) : kindLabel(k.toLowerCase()), filter, reason))
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -94,6 +106,7 @@ const renderFilters = (q, payload = {}, kindsPresent = []) => {
|
|||
renderModuleStats(matches.length),
|
||||
form({ method: "GET", action: "/data", class: "filter-box" },
|
||||
reason ? input({ type: "hidden", name: "reason", value: reason }) : null,
|
||||
filter !== "ALL" ? input({ type: "hidden", name: "filter", value: filter }) : null,
|
||||
input({ type: "text", name: "q", value: q || "", placeholder: i18n.dataSearchPlaceholder, class: "filter-box__input" }),
|
||||
div({ class: "filter-box__controls" },
|
||||
button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)
|
||||
|
|
@ -165,7 +178,7 @@ const renderMatchRow = (m, isBest) => {
|
|||
span({ class: "data-kind-chip" }, String(kindLabel(m.kind)).toUpperCase()),
|
||||
span({ class: "card-label data-score-label" }, `${isBest ? i18n.dataBestMatch : (i18n.dataCcShort || "CC")}: `),
|
||||
span({ class: isBest ? "data-score-value data-best-score" : "data-score-value" }, `${scorePct(m.score)}%`),
|
||||
a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, "↗")
|
||||
a({ href: m.href, class: "btn-singleview btn-content data-side-visit", title: i18n.visitContent }, renderIcon("open"))
|
||||
),
|
||||
a({ href: m.href, class: "data-card-title" }, shortTitle(m.title) || shortId(m.id)),
|
||||
span({ class: "data-card-author" }, userLink(m.author)),
|
||||
|
|
@ -210,12 +223,12 @@ exports.dataView = async (payload = {}) => {
|
|||
return template(
|
||||
i18n.dataTitle,
|
||||
section(div({ class: "tags-header module-header-line" }, h2(i18n.dataTitle), p(i18n.dataDescription))),
|
||||
renderFilters(q, { ...payload, matches }, groups.map(g => g.kind)),
|
||||
renderFilters(q, { ...payload, matches }, payload.kindsMatched && typeof payload.kindsMatched === "object" ? Object.keys(payload.kindsMatched) : groups.map(g => g.kind)),
|
||||
emptyData ? null : (payload.cohesion ? renderCohesion(payload.cohesion) : null),
|
||||
section(
|
||||
emptyData ? null : div({ class: "tags-header" },
|
||||
h2(q ? `${i18n.dataTopicTitle} #${q}` : i18n.dataMatchesTitle),
|
||||
p(q ? i18n.dataTopicHint : i18n.dataMatchesHint)
|
||||
h2(i18n.dataMatchesTitle),
|
||||
p(i18n.dataMatchesHint)
|
||||
),
|
||||
best
|
||||
? div({ class: "data-section data-top-best" },
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink }
|
|||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderStyledText } = require("../backend/renderStyledText");
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para
|
|||
div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, doc.tribeOrigin ? renderTribeOriginChip(doc.tribeOrigin) : doc.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('documents', doc.title, doc.key)) : null, renderLicenseChip(doc.license), ...(typeof params.titleChips === "function" ? params.titleChips(doc) : [])),
|
||||
doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null,
|
||||
doc?.url
|
||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` })
|
||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) })
|
||||
: p(i18n.documentNoFile),
|
||||
...(typeof params.bodyExtra === "function" ? params.bodyExtra(doc) : []),
|
||||
engagement,
|
||||
|
|
@ -180,7 +180,6 @@ exports.documentView = async (documents, filter = "all", documentId = null, para
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
||||
|
|
@ -285,7 +284,7 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
|||
const docMain = div({ class: "tribe-main" },
|
||||
detailActions,
|
||||
doc?.url
|
||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": `/blob/${encodeURIComponent(doc.url)}` })
|
||||
? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) })
|
||||
: p(i18n.documentNoFile),
|
||||
(() => {
|
||||
const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN;
|
||||
|
|
@ -321,7 +320,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params =
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/documents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterAll).toUpperCase()),
|
||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.documentFilterRecent).toUpperCase())] : []),
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
|||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/emergencies", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
...(emptyMod ? [] : [
|
||||
...chipButtons(BASE_FILTERS, filter, census, baseLabel),
|
||||
...chipButtons(SEVERITIES, filter, census, sevLabel),
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const renderFeedCard = exports.renderFeedCard = (feed, spreadMap = null, opts =
|
|||
const signerId = feed.value.author || "";
|
||||
const refeedsNum = Number(content.refeeds || 0) || 0;
|
||||
const commentCount = Number(content.commentCount || 0);
|
||||
const styledNodes = renderStyledText(safeText, { zoomImages: true });
|
||||
const styledNodes = renderStyledText(safeText, { zoomImages: true, ...(typeof opts.hashtagHref === 'function' ? { hashtagHref: opts.hashtagHref } : {}) });
|
||||
const refeedAction = opts.refeedAction || `/feed/refeed/${encodeURIComponent(feed.key)}`;
|
||||
const canRefeed = opts.canRefeed !== false && !(authorId && String(authorId) === String(me));
|
||||
const headerActions = opts.headerActions !== undefined
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const renderFileSeeds = (t, spreadMap) => {
|
|||
const info = spreadInfoOf(t, spreadMap);
|
||||
return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0));
|
||||
};
|
||||
const renderFileSpread = (t, spreadMap) => String(t.author) === String(userId) ? "" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
||||
const renderFileSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
||||
|
||||
const renderFileTable = exports.renderFileTable = (files, filter, params = {}) => {
|
||||
const returnTo = buildReturnTo(filter, params);
|
||||
|
|
@ -86,18 +86,16 @@ const renderFileTable = exports.renderFileTable = (files, filter, params = {}) =
|
|||
{ border: "1", class: "file-table" },
|
||||
tr(
|
||||
th(i18n.createdAt || "DATE"),
|
||||
th(i18n.authorLabel || "AUTHOR"),
|
||||
th(i18n.fileTitleLabel || "TITLE"),
|
||||
th(i18n.fileSizeLabel || "SIZE"),
|
||||
th(i18n.seedsLabel),
|
||||
th(i18n.seedAction),
|
||||
th({ class: "torrent-spread-cell" }, i18n.seedsLabel),
|
||||
th({ class: "torrent-spread-cell" }, i18n.seedAction),
|
||||
th("")
|
||||
),
|
||||
files.map((t) =>
|
||||
tr(
|
||||
td({ class: "row-chips" }, t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('files', t.title, t.key)) : null),
|
||||
td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
td({ class: "row-author" }, userLink(t.author)),
|
||||
td({ class: "row-title", "data-label": i18n.fileTitleLabel || "TITLE" }, t.title || ""),
|
||||
td({ "data-label": i18n.fileSizeLabel || "SIZE" }, formatSize(t.size)),
|
||||
td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, renderFileSeeds(t, params.spreadMap)),
|
||||
|
|
@ -226,7 +224,6 @@ exports.filesView = async (files, filter = "all", fileId = null, params = {}) =>
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()),
|
||||
|
|
@ -388,7 +385,7 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params =
|
|||
: null
|
||||
);
|
||||
})(),
|
||||
renderEngagement(fileObj.key,
|
||||
fileObj.tribeId ? null : renderEngagement(fileObj.key,
|
||||
renderOpinionsVoting('/files/opinions', fileObj.key, fileObj.opinions, returnTo, fileObj.opinions_inhabitants),
|
||||
renderFileCommentsSection(fileObj.key, comments, returnTo)
|
||||
)
|
||||
|
|
@ -405,7 +402,6 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params =
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/files", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterAll).toUpperCase()),
|
||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.fileFilterRecent).toUpperCase())] : []),
|
||||
|
|
|
|||
|
|
@ -94,9 +94,12 @@ const buildGraphSvg = (me, peers, links = []) => {
|
|||
+ `</svg>`;
|
||||
};
|
||||
|
||||
const zoomableGraph = (id, svg) => [
|
||||
div({ class: 'graphos-zoom-row' }, a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, `\u2922 ${i18n.graphosZoom || 'Enlarge'}`)),
|
||||
div({ class: 'graphos-canvas', [RAW_HTML]: svg }),
|
||||
const zoomableGraph = (id, svg, stats = []) => [
|
||||
div({ class: 'graphos-legend graphos-statsbar' }, ...stats.filter(Boolean)),
|
||||
div({ class: 'graphos-canvas' },
|
||||
a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, `\u2922 ${i18n.graphosZoom || 'Enlarge'}`),
|
||||
div({ class: 'graphos-canvas-svg', [RAW_HTML]: svg })
|
||||
),
|
||||
div({ id, class: 'lightbox graphos-lightbox' },
|
||||
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
||||
div({ class: 'graphos-canvas-zoom' },
|
||||
|
|
@ -108,7 +111,7 @@ const zoomableGraph = (id, svg) => [
|
|||
)
|
||||
];
|
||||
|
||||
const kpi = (label, value) => div({ class: 'stats-kpi stats-kpi-inline' },
|
||||
const kpi = (label, value) => span({ class: 'graphos-legend-item graphos-kpi' },
|
||||
span({ class: 'stats-kpi-label' }, String(label).toUpperCase()),
|
||||
span({ class: 'stats-kpi-value' }, String(value))
|
||||
);
|
||||
|
|
@ -153,15 +156,12 @@ exports.graphosView = ({ filter, me, peers, links = [], kpis, focus = null, focu
|
|||
capped
|
||||
? p({ class: 'graphos-cap-note' }, `${i18n.graphosShowing || 'Showing'} ${shown} / ${total}`)
|
||||
: null,
|
||||
div({ class: 'stats-block graphos-stats' },
|
||||
div({ class: 'stats-grid' },
|
||||
kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total),
|
||||
kpi(i18n.online || 'Online', kpis.online),
|
||||
filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null,
|
||||
filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null
|
||||
)
|
||||
),
|
||||
...zoomableGraph('graphos-zoom', buildGraphSvg(me, peers, links)),
|
||||
...zoomableGraph('graphos-zoom', buildGraphSvg(me, peers, links), [
|
||||
kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total),
|
||||
kpi(i18n.online || 'Online', kpis.online),
|
||||
filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null,
|
||||
filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null
|
||||
]),
|
||||
div({ class: 'graphos-legend' },
|
||||
legendItem('me', i18n.graphosYou || 'You'),
|
||||
legendItem('online', i18n.online || 'Online'),
|
||||
|
|
@ -267,18 +267,15 @@ exports.graphosFederationView = ({ myName, rows = [], links = [], ownVersion = '
|
|||
)
|
||||
)
|
||||
),
|
||||
div({ class: 'stats-block graphos-stats' },
|
||||
div({ class: 'stats-grid' },
|
||||
kpi(i18n.graphosFedPubs, sorted.length),
|
||||
kpi(i18n.peerStateConnected, count(r => r.state === 'connected')),
|
||||
kpi(i18n.graphosFedUnreachable, count(r => r.state === 'unreachable')),
|
||||
kpi(i18n.peerOutdated, count(outdated)),
|
||||
kpi(i18n.peerErrTor, count(r => r.transport === 'tor')),
|
||||
kpi(i18n.graphosFedLinks, links.filter(l => l.mutual).length),
|
||||
kpi(i18n.graphosFedIsolated, count(r => r.isolated))
|
||||
)
|
||||
),
|
||||
...(sorted.length ? zoomableGraph('graphos-fed-zoom', buildFederationSvg(myName || '', sorted, links, ownVersion)) : [p(i18n.graphosFedEmpty)]),
|
||||
...(sorted.length ? zoomableGraph('graphos-fed-zoom', buildFederationSvg(myName || '', sorted, links, ownVersion), [
|
||||
kpi(i18n.graphosFedPubs, sorted.length),
|
||||
kpi(i18n.peerStateConnected, count(r => r.state === 'connected')),
|
||||
kpi(i18n.graphosFedUnreachable, count(r => r.state === 'unreachable')),
|
||||
kpi(i18n.peerOutdated, count(outdated)),
|
||||
kpi(i18n.peerErrTor, count(r => r.transport === 'tor')),
|
||||
kpi(i18n.graphosFedLinks, links.filter(l => l.mutual).length),
|
||||
kpi(i18n.graphosFedIsolated, count(r => r.isolated))
|
||||
]) : [p(i18n.graphosFedEmpty)]),
|
||||
div({ class: 'graphos-legend' },
|
||||
legendItem('me', i18n.graphosYou || 'You'),
|
||||
span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-dot graphos-fed-dot-connected' }), span(i18n.peerStateConnected)),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink }
|
|||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderStyledText } = require("../backend/renderStyledText")
|
||||
const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
||||
|
|
@ -36,7 +36,7 @@ const renderTags = (tags) => {
|
|||
};
|
||||
|
||||
const renderImageMedia = (imgObj, filter, params = {}) => {
|
||||
const src = imgObj?.url ? `/blob/${encodeURIComponent(imgObj.url)}` : "";
|
||||
const src = imgObj?.url ? blobUrlOf(imgObj.url) : "";
|
||||
|
||||
return imgObj?.url
|
||||
? div(
|
||||
|
|
@ -144,7 +144,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => {
|
|||
br(),
|
||||
br(),
|
||||
imageToEdit?.url
|
||||
? img({ loading: 'lazy', src: `/blob/${encodeURIComponent(imageToEdit.url)}`, class: "media-preview", alt: imageToEdit?.title || "" })
|
||||
? img({ loading: 'lazy', src: blobUrlOf(imageToEdit.url), class: "media-preview", alt: imageToEdit?.title || "" })
|
||||
: null,
|
||||
label(i18n.imageTitleLabel),
|
||||
br(),
|
||||
|
|
@ -188,7 +188,7 @@ const renderGallery = (images) => {
|
|||
|
||||
const renderLightbox = (images) =>
|
||||
images.map((imgObj) => {
|
||||
const src = imgObj.url ? `/blob/${encodeURIComponent(imgObj.url)}` : "";
|
||||
const src = imgObj.url ? blobUrlOf(imgObj.url) : "";
|
||||
return div(
|
||||
{ id: `img-${encodeURIComponent(imgObj.key)}`, class: "lightbox" },
|
||||
a({ href: `#img-${encodeURIComponent(imgObj.key)}-src`, class: "lightbox-close" }, "×"),
|
||||
|
|
@ -239,7 +239,6 @@ exports.imageView = async (images, filter = "all", imageId = null, params = {})
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()),
|
||||
|
|
@ -351,7 +350,7 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
|||
? div(
|
||||
{ class: "image-container" },
|
||||
img({
|
||||
src: `/blob/${encodeURIComponent(imageObj.url)}`,
|
||||
src: blobUrlOf(imageObj.url),
|
||||
alt: imageObj.title || "",
|
||||
class: "media-preview",
|
||||
loading: "lazy"
|
||||
|
|
@ -392,7 +391,6 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []),
|
||||
|
|
|
|||
|
|
@ -160,11 +160,6 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
|||
fediverseHandle: typeof raw.fediverseHandle === 'string' ? raw.fediverseHandle : ''
|
||||
};
|
||||
return div({ class: 'trending-card inhabitants-card' + (isMe ? ' own-content' : '') },
|
||||
div(
|
||||
{ class: 'card-header activity-card-header' },
|
||||
span(),
|
||||
renderContentActions(user.id, `/inhabitant/${encodeURIComponent(user.id)}`, { author: user.id })
|
||||
),
|
||||
div({ class: 'card-section inhabitants-card-body' },
|
||||
div({ class: 'inhabitant-card' },
|
||||
div({ class: 'inhabitant-left' },
|
||||
|
|
@ -175,7 +170,7 @@ const renderInhabitantCard = (user, filter, currentUserId, fediverseConfigured)
|
|||
user.name ? p({ class: 'inhabitant-left-name' }, userLink(user.id, user.name)) : null,
|
||||
...renderUserSensors({
|
||||
isMe, fediverseConfigured, prefs, id: user.id,
|
||||
karmaScore: user.karmaScore, carbonGrams: user.carbonGrams,
|
||||
karmaScore: user.karmaScore, karmaReceived: user.karmaReceived || null, carbonGrams: user.carbonGrams,
|
||||
deviceSource: user.deviceSource, activityBucket: user.lastActivityBucket,
|
||||
gpgFingerprint: user.gpgFingerprint, ecoAddress: user.ecoAddress,
|
||||
estimatedUBI: user.estimatedUBI, lastClaimedDate: user.lastClaimedDate, totalClaimed: user.totalClaimed,
|
||||
|
|
@ -411,7 +406,7 @@ exports.inhabitantsProfileView = (payload, currentUserId, fediverseConfigured) =
|
|||
safe.oasisVersion ? div({ class: 'profile-side-version' }, '🌴 ' + (i18n.oasisVersionLabel || 'Oasis Version') + ': ', strong(safe.oasisVersion)) : null,
|
||||
...renderUserSensors({
|
||||
isMe, fediverseConfigured, prefs, id: id || viewedId,
|
||||
karmaScore, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket,
|
||||
karmaScore, karmaReceived: safe.karmaReceived || null, carbonGrams, deviceSource: safe.deviceSource, activityBucket: providedBucket,
|
||||
gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed,
|
||||
larpHouse, stats: safe.stats,
|
||||
mutual: !!(safe.relationship && safe.relationship.following && safe.relationship.followsMe)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { div, h1, h2, h3, p, section, button, form, input, span, img, a, br, table, tr, td, textarea, label, strong, details, summary } = require("../server/node_modules/hyperaxe");
|
||||
const { renderStyledText } = require("../backend/renderStyledText");
|
||||
const { template, i18n, userLink, paged } = require("./main_views");
|
||||
const { template, i18n, userLink, paged, renderIcon } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
|
||||
const userId = config.keys.id;
|
||||
|
|
@ -209,7 +209,7 @@ const renderPostsBlock = (posts, house, canPost) => {
|
|||
paged(stream).map(entry => div({ class: entry.post.author === userId ? 'larp-post own-comment' : 'larp-post' },
|
||||
entry.post.author === userId && entry.post.id
|
||||
? form({ method: 'POST', action: `/larp/post/delete/${encodeURIComponent(entry.post.id)}`, class: 'comment-delete-form' },
|
||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, '✕'))
|
||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close')))
|
||||
: null,
|
||||
div({ class: 'larp-post-head' },
|
||||
userLink(entry.post.author),
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@ const renderFilters = (filter, q, zone, census, zones, emptyMod) =>
|
|||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/logistics", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
input({ type: "hidden", name: "zone", value: zone || "" }),
|
||||
...(emptyMod ? [] : [
|
||||
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
|||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/mailing", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
...(emptyMod ? [] : [
|
||||
...chipButtons(BASE_FILTERS, filter, census, baseLabel),
|
||||
...chipButtons(TYPE_FILTERS, filter, census, typeLabel),
|
||||
|
|
|
|||
|
|
@ -111,18 +111,27 @@ const renderLiveFeed = (src, imgClass) => {
|
|||
a({ href: `#${id}`, id: `${id}-src`, class: 'zoom-link zoom-inline' }, img({ src, alt: '', class: imgClass })),
|
||||
div({ id, class: 'lightbox live-lightbox' },
|
||||
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
||||
a({ href: `#${id}-full`, class: 'live-fullscreen-btn', title: i18n.liveFullscreen }, renderIcon('fullscreen')),
|
||||
a({ href: `#${id}-src`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' }))
|
||||
),
|
||||
div({ id: `${id}-full`, class: 'lightbox live-lightbox live-fullscreen' },
|
||||
a({ href: `#${id}`, class: 'lightbox-close' }, '\u00d7'),
|
||||
a({ href: `#${id}`, class: 'lightbox-image-link' }, img({ src, alt: '', class: 'lightbox-image', loading: 'lazy' }))
|
||||
)
|
||||
];
|
||||
};
|
||||
exports.renderLiveFeed = renderLiveFeed;
|
||||
|
||||
const renderVideoGrid = (peers, me) => {
|
||||
const renderVideoGrid = (peers, me, { film = false } = {}) => {
|
||||
const list = (Array.isArray(peers) ? peers : []).filter(p => p && p.id);
|
||||
const filmName = (...label) => span({ class: 'video-tile-name video-tile-film' }, '▶ ', ...label);
|
||||
const tiles = [
|
||||
me ? div({ class: 'video-tile video-tile-me' }, ...renderLiveFeed('/phone/video/me', 'video-feed'), span({ class: 'video-tile-name' }, i18n.roomYou)) : null,
|
||||
...(Array.isArray(peers) ? peers : []).filter(p => p && p.id && p.on).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(p.id)}`, 'video-feed'), userLink(p.id)))
|
||||
film ? div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent('film:me')}`, 'video-feed'), filmName(i18n.roomYou)) : null,
|
||||
...list.filter(p => p.on).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(p.id)}`, 'video-feed'), userLink(p.id))),
|
||||
...list.filter(p => p.film).map(p => div({ class: 'video-tile' }, ...renderLiveFeed(`/phone/video/${encodeURIComponent(`film:${p.id}`)}`, 'video-feed'), filmName(userLink(p.id))))
|
||||
].filter(Boolean);
|
||||
return tiles.length ? div({ class: 'video-grid' }, ...tiles) : null;
|
||||
return tiles.length ? div({ class: tiles.length === 1 ? 'video-grid video-grid-solo' : 'video-grid' }, ...tiles) : null;
|
||||
};
|
||||
exports.renderVideoGrid = renderVideoGrid;
|
||||
|
||||
|
|
@ -148,7 +157,7 @@ const renderSubscriptionBox = ({ target, scope, subscribed, count, isOwner, retu
|
|||
: button({ type: 'submit', class: subscribed ? 'tribe-action-btn danger-btn' : 'tribe-action-btn' }, `${subscribed ? '🔕' : '🔔'} ${String(subscribed ? i18n.subscriptionUnsubscribe : i18n.subscriptionSubscribe).toUpperCase()}`)
|
||||
),
|
||||
showPm && total
|
||||
? a({ href: `/pm?list=${encodeURIComponent(target)}`, class: 'btn-singleview btn-pm', title: i18n.pmCreateButton || 'Write a PM' }, '✉')
|
||||
? a({ href: `/pm?list=${encodeURIComponent(target)}`, class: 'btn-singleview btn-pm', title: i18n.pmCreateButton || 'Write a PM' }, renderIcon('mail'))
|
||||
: null
|
||||
)
|
||||
];
|
||||
|
|
@ -254,7 +263,7 @@ const renderLinkBox = (blockId, viewHref, title) => {
|
|||
const id = `linkbox-${linkBoxSeq}-${String(blockId).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`;
|
||||
const clearnetHref = clearnetHrefFor(viewHref, blockId, title);
|
||||
return [
|
||||
a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, '⚯'),
|
||||
a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, renderIcon('link')),
|
||||
span({ id, class: 'lightbox link-lightbox' },
|
||||
a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'),
|
||||
span({ class: 'link-box' },
|
||||
|
|
@ -297,6 +306,29 @@ const renderFileDownloads = (fileKey, torrentUrl, name, opts = {}) => {
|
|||
};
|
||||
exports.renderFileDownloads = renderFileDownloads;
|
||||
|
||||
const blobUrlOf = (v) => (String(v || '').startsWith('/blob/sealed/') ? String(v) : `/blob/${encodeURIComponent(v)}`);
|
||||
exports.blobUrlOf = blobUrlOf;
|
||||
const ACTION_ICONS = {
|
||||
hide: '<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/>',
|
||||
spread: '<path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/>',
|
||||
pin: '<path d="M9 3h6l-1 6 4 4H6l4-4-1-6z"/><line x1="12" y1="13" x2="12" y2="21"/>',
|
||||
report: '<line x1="5" y1="21" x2="5" y2="3"/><path d="M5 4h12l-2.5 4.5L17 13H5"/>',
|
||||
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><polyline points="3 7 12 13 21 7"/>',
|
||||
call: '<path d="M5 3h4l2 5-2.5 1.5a11 11 0 0 0 6 6L16 13l5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 3 5a2 2 0 0 1 2-2z"/>',
|
||||
link: '<path d="M10 14a4.5 4.5 0 0 0 6.4 0l3.2-3.2a4.5 4.5 0 0 0-6.4-6.4L11.8 5.8"/><path d="M14 10a4.5 4.5 0 0 0-6.4 0l-3.2 3.2a4.5 4.5 0 0 0 6.4 6.4l1.4-1.4"/>',
|
||||
block: '<path d="M12 2.5l8.5 4.75v9.5L12 21.5l-8.5-4.75v-9.5z"/><polyline points="3.5 7.25 12 12 20.5 7.25"/><line x1="12" y1="12" x2="12" y2="21.5"/>',
|
||||
datagram: '<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/>',
|
||||
open: '<path d="M7 17L17 7M8 7h9v9"/>',
|
||||
donate: '<line x1="12" y1="2" x2="12" y2="22"/><line x1="3.3" y1="7" x2="20.7" y2="17"/><line x1="3.3" y1="17" x2="20.7" y2="7"/><polyline points="9.5 4 12 6 14.5 4"/><polyline points="9.5 20 12 18 14.5 20"/>',
|
||||
torrent: '<line x1="7" y1="20" x2="7" y2="4"/><polyline points="3 8 7 4 11 8"/><line x1="17" y1="4" x2="17" y2="20"/><polyline points="13 16 17 20 21 16"/>',
|
||||
close: '<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>',
|
||||
film: '<rect x="3" y="4" width="18" height="16" rx="2"/><line x1="7" y1="4" x2="7" y2="20"/><line x1="17" y1="4" x2="17" y2="20"/><line x1="3" y1="12" x2="21" y2="12"/>',
|
||||
fullscreen: '<polyline points="4 9 4 4 9 4"/><polyline points="15 4 20 4 20 9"/><polyline points="20 15 20 20 15 20"/><polyline points="9 20 4 20 4 15"/>'
|
||||
};
|
||||
const renderIcon = (name) => span({ class: 'btn-icon', [RAW_HTML]: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ACTION_ICONS[name] || ''}</svg>` });
|
||||
const renderHideIcon = () => renderIcon('hide');
|
||||
exports.renderIcon = renderIcon;
|
||||
exports.renderHideIcon = renderHideIcon;
|
||||
const renderContentActions = (msgId, viewHref, opts = {}) => {
|
||||
const o = (opts && typeof opts === 'object') ? opts : {};
|
||||
const blockId = (typeof msgId === 'string' && msgId.startsWith('%')) ? msgId : null;
|
||||
|
|
@ -306,7 +338,7 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
|||
const pinBtn = o.favKind && blockId
|
||||
? form({ method: 'POST', action: `/${o.favKind}/favorites/${o.isFavorite ? 'remove' : 'add'}/${encodeURIComponent(blockId)}`, class: 'content-action-form' },
|
||||
o.returnTo ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
||||
button({ type: 'submit', class: o.isFavorite ? 'btn-singleview btn-pin-on' : 'btn-singleview', title: o.isFavorite ? i18n.favoriteRemove : i18n.favoriteAdd }, o.isFavorite ? '✜' : '✛')
|
||||
button({ type: 'submit', class: o.isFavorite ? 'btn-singleview btn-pin-on' : 'btn-singleview', title: o.isFavorite ? i18n.favoriteRemove : i18n.favoriteAdd }, renderIcon('pin'))
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
@ -318,17 +350,17 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
|||
if (!src || typeof src.blobId !== 'string' || !src.blobId.startsWith('&')) return null;
|
||||
try { if (getConfig().modules.torrentsMod !== 'on') return null; } catch (_) { return null; }
|
||||
const name = String(src.name || o.reportTitle || '').slice(0, 100);
|
||||
return a({ href: `/torrents?filter=create&fromBlob=${encodeURIComponent(src.blobId)}&name=${encodeURIComponent(name)}`, class: 'btn-singleview btn-torrent', title: i18n.torrentFromContent }, '\u21C5');
|
||||
return a({ href: `/torrents?filter=create&fromBlob=${encodeURIComponent(src.blobId)}&name=${encodeURIComponent(name)}`, class: 'btn-singleview btn-torrent', title: i18n.torrentFromContent }, renderIcon('torrent'));
|
||||
})();
|
||||
|
||||
const chainBtn = blockId && process.env.OASIS_MOBILE !== '1'
|
||||
? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, '⦿')
|
||||
? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, renderIcon('block'))
|
||||
: null;
|
||||
|
||||
const herePath = (listScope() || {}).path;
|
||||
const onItself = (() => { try { return !!herePath && decodeURIComponent(String(viewHref || '').split(/[?#]/)[0]) === decodeURIComponent(String(herePath)); } catch (_) { return false; } })();
|
||||
const contentBtn = viewHref && !onItself
|
||||
? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, '↗')
|
||||
? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, renderIcon('open'))
|
||||
: null;
|
||||
|
||||
const donateBtn = (() => {
|
||||
|
|
@ -337,32 +369,41 @@ const renderContentActions = (msgId, viewHref, opts = {}) => {
|
|||
const donatable = sharedState.getDonatableAuthors ? sharedState.getDonatableAuthors() : null;
|
||||
if (!donatable || !donatable.has(String(o.author))) return null;
|
||||
const ref = viewHref ? `?returnTo=${encodeURIComponent(viewHref)}` : '';
|
||||
return a({ href: `/wallet/donate/${encodeURIComponent(o.author)}${ref}`, class: 'btn-singleview btn-donate', title: i18n.donateTooltip }, '\u2744');
|
||||
return a({ href: `/wallet/donate/${encodeURIComponent(o.author)}${ref}`, class: 'btn-singleview btn-donate', title: i18n.donateTooltip }, renderIcon('donate'));
|
||||
})();
|
||||
|
||||
const reportHref = blockId
|
||||
? `/reports?filter=create&category=CONTENT&title=${encodeURIComponent(String(o.reportTitle || blockId).slice(0, 120))}&description=${encodeURIComponent(`${viewHref || blockId}`)}`
|
||||
: null;
|
||||
const reportBtn = o.report !== false && reportHref && !(o.author && String(o.author) === String(myId))
|
||||
? a({ href: reportHref, class: 'btn-singleview btn-report', title: i18n.reportContent }, '⚑')
|
||||
? a({ href: reportHref, class: 'btn-singleview btn-report', title: i18n.reportContent }, renderIcon('report'))
|
||||
: null;
|
||||
|
||||
const pmBtn = o.author && String(o.author) !== String(myId)
|
||||
? a({ href: `/pm?recipients=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-pm', title: i18n.pmContentTooltip }, '✉')
|
||||
const pmBtn = o.contact === true && o.author && String(o.author) !== String(myId)
|
||||
? a({ href: `/pm?recipients=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-pm', title: i18n.pmContentTooltip }, renderIcon('mail'))
|
||||
: null;
|
||||
const callBtn = o.author && String(o.author) !== String(myId) && phoneOn() && !sharedState.isPubId(o.author)
|
||||
? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, '✆')
|
||||
const callBtn = o.contact === true && o.author && String(o.author) !== String(myId) && phoneOn() && !sharedState.isPubId(o.author)
|
||||
? a({ href: `/phone?filter=create&to=${encodeURIComponent(o.author)}`, class: 'btn-singleview btn-call', title: i18n.phoneCallTooltip }, renderIcon('call'))
|
||||
: null;
|
||||
|
||||
const deleteBtn = o.deleteAction
|
||||
? form({ method: 'POST', action: o.deleteAction, class: 'content-action-form content-action-delete' },
|
||||
o.returnTo && o.returnTo !== viewHref ? input({ type: 'hidden', name: 'returnTo', value: o.returnTo }) : null,
|
||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, '✕')
|
||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.chatDelete }, renderIcon('close'))
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn) return null;
|
||||
return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn);
|
||||
const hideBtn = blockId && o.hide !== false && !(o.author && String(o.author) === String(myId))
|
||||
? form({ method: 'POST', action: `/hide/${encodeURIComponent(blockId)}`, class: 'content-action-form' },
|
||||
input({ type: 'hidden', name: 'returnTo', value: o.returnTo || viewHref || '/' }),
|
||||
viewHref ? input({ type: 'hidden', name: 'href', value: viewHref }) : null,
|
||||
o.reportTitle ? input({ type: 'hidden', name: 'label', value: String(o.reportTitle).slice(0, 120) }) : null,
|
||||
button({ type: 'submit', class: 'btn-singleview btn-hide', title: i18n.hideContent }, renderHideIcon())
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null;
|
||||
return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn);
|
||||
};
|
||||
exports.renderContentActions = renderContentActions;
|
||||
const CONTENT_FAV_KIND = {
|
||||
|
|
@ -416,8 +457,9 @@ exports.renderDocumentActions = renderDocumentActions;
|
|||
const renderRelationshipBlock = (relationship, feedId) => {
|
||||
const rel = relationship || {};
|
||||
if (rel.me) return span({ class: 'status you' }, i18n.relationshipYou);
|
||||
const muted = (() => { try { return require('../models/viewer_filters').isMuted(feedId); } catch (_) { return false; } })();
|
||||
const actions = [];
|
||||
const icons = { follow: '♥', unfollow: '✕', block: '⊘', unblock: '↺' };
|
||||
const icons = { follow: '♥', unfollow: '✕', block: '⊘', unblock: '↺', mute: '◌', unmute: '●' };
|
||||
const addAction = (action) => actions.push(
|
||||
form({ action: `/${action}/${encodeURIComponent(feedId)}`, method: 'post' },
|
||||
button({ type: 'submit', class: 'filter-btn' }, `${icons[action]} ${i18n[action]}`)
|
||||
|
|
@ -426,7 +468,9 @@ const renderRelationshipBlock = (relationship, feedId) => {
|
|||
if (rel.following) addAction('unfollow');
|
||||
else if (rel.blocking) addAction('unblock');
|
||||
else { addAction('follow'); addAction('block'); }
|
||||
if (!rel.blocking) addAction(muted ? 'unmute' : 'mute');
|
||||
return div({ class: 'relationship-status' },
|
||||
muted ? span({ class: 'status blocked' }, i18n.relationshipMuted) : null,
|
||||
rel.blocking && rel.blockedBy
|
||||
? span({ class: 'status blocked' }, i18n.relationshipMutualBlock)
|
||||
: [
|
||||
|
|
@ -686,7 +730,7 @@ const renderSpreadButton = (msgKey, opts, title = null, showCount = true) => {
|
|||
{ method: 'POST', action: `/spread/${encodeURIComponent(msgKey)}`, class: 'spread-form' },
|
||||
button(
|
||||
{ type: 'submit', class: alreadySpread ? 'btn-singleview btn-spread-on' : 'btn-singleview', title: title || i18n.spreadContent },
|
||||
showCount ? `⟳ ${count}` : '⟳'
|
||||
renderIcon('spread'), showCount ? span({ class: 'btn-count' }, String(count)) : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
@ -1946,7 +1990,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
return renderEmergencyBanner(featured, { compact });
|
||||
} catch (_) { return null; }
|
||||
};
|
||||
const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : "blocks";
|
||||
const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks";
|
||||
const themeLink = link({
|
||||
rel: "stylesheet",
|
||||
href: `/assets/themes/${theme}.css?v=${assetVersion()}`
|
||||
|
|
@ -2039,7 +2083,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
);
|
||||
})(),
|
||||
renderPhoneTopLink(),
|
||||
(uxMode === "chats" || uxMode === "feed" || uxMode === "phone")
|
||||
(uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms")
|
||||
? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings })
|
||||
: null
|
||||
)
|
||||
|
|
@ -2092,7 +2136,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
navLink({ href: "/peers", emoji: "⧖", text: i18n.peers })
|
||||
)
|
||||
)
|
||||
) : (uxMode === "chats" || uxMode === "feed" || uxMode === "phone") ? div(
|
||||
) : (uxMode === "chats" || uxMode === "feed" || uxMode === "phone" || uxMode === "rooms") ? div(
|
||||
{ class: "top-bar-right" },
|
||||
nav(
|
||||
ul(
|
||||
|
|
@ -2151,7 +2195,7 @@ const template = (titlePrefix, ...elements) => {
|
|||
buildPhoneBanner(),
|
||||
buildEmergencyBanner(false),
|
||||
div(
|
||||
{ class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : uxMode === "feed" || uxMode === "phone" ? "main-content chatsux-only" : "main-content" },
|
||||
{ class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : uxMode === "feed" || uxMode === "phone" || uxMode === "rooms" ? "main-content chatsux-only" : "main-content" },
|
||||
uxMode !== "blocks" ? null : div(
|
||||
{ class: "sidebar-left" },
|
||||
nav(
|
||||
|
|
@ -2868,6 +2912,15 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => {
|
|||
p({ class: "post-text" }, ...renderStyledText(rawText))
|
||||
);
|
||||
}
|
||||
const postTags = String(content.type || '') === 'post' && Array.isArray(content.tags) ? content.tags.filter(Boolean) : [];
|
||||
if (postTags.length) {
|
||||
articleElement = div(
|
||||
articleElement,
|
||||
div({ class: 'card-tags card-tags-mt' },
|
||||
...postTags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
return section(
|
||||
|
|
@ -3158,8 +3211,8 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = '
|
|||
);
|
||||
};
|
||||
|
||||
exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt }) => {
|
||||
const { escapeHtml: esc, renderKindTag, renderClearnetPage, renderRichText } = require('./clearnet_view');
|
||||
exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWarning, sentAt, tags = [] }) => {
|
||||
const { escapeHtml: esc, renderKindTag, renderClearnetPage, renderRichText, renderTagChips } = require('./clearnet_view');
|
||||
const rawText = String(text || '');
|
||||
const renderedHtml = sanitizeHtml(renderRichText(rawText));
|
||||
const plainPreview = rawText.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/<[^>]+>/g, '').slice(0, 200);
|
||||
|
|
@ -3189,6 +3242,7 @@ exports.clearnetBlogView = async ({ msgKey, text, author, authorName, contentWar
|
|||
${cw ? `<div class="cn-blog-cw">${cw}</div>` : ''}
|
||||
<hr class="cn-sep"/>
|
||||
<article class="cn-blog-body">${renderedHtml}</article>
|
||||
${renderTagChips(tags)}
|
||||
`;
|
||||
return renderClearnetPage({
|
||||
title: `${titleText} | Oasis`,
|
||||
|
|
@ -3334,7 +3388,7 @@ const buildClearnetHub = ({ items = {}, filterBase, filterType = '', query = '',
|
|||
exports.CLEARNET_HUB_CSS = CLEARNET_HUB_CSS;
|
||||
|
||||
exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filterType = '', query = '', page = 1, tribesPage = 1 }) => {
|
||||
const { escapeHtml: esc, renderClearnetPage, blobUrl: cnBlob, CLEARNET_PAGER_CSS, renderClearnetPager } = require('./clearnet_view');
|
||||
const { escapeHtml: esc, renderClearnetPage, renderClearnetHubSearch, blobUrl: cnBlob, CLEARNET_PAGER_CSS, renderClearnetPager } = require('./clearnet_view');
|
||||
const hub = buildClearnetHub({ items, filterBase: '/c', filterType, query, page });
|
||||
const extraCss = CLEARNET_HUB_CSS + CLEARNET_PAGER_CSS + `
|
||||
`;
|
||||
|
|
@ -3361,7 +3415,7 @@ exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filter
|
|||
ogDescription: i18n.cnHubDescription,
|
||||
extraCss,
|
||||
body,
|
||||
headerExtra: `<form class="cn-search" method="GET" action="/c">${filterType ? `<input type="hidden" name="type" value="${esc(filterType)}"/>` : ''}<input type="text" name="q" value="${esc(query || '')}" placeholder="${esc(i18n.cnSearchPlaceholder)}" autocomplete="off"/></form>`
|
||||
headerExtra: renderClearnetHubSearch({ type: filterType, query })
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -3516,7 +3570,17 @@ const renderUserSensors = (u, opts = {}) => {
|
|||
})()
|
||||
: null;
|
||||
const items = [];
|
||||
if (show('karma')) items.push(span({ class: 'karma-line' }, `${i18n.bankingUserEngagementScore}: `, strong(String(u.karmaScore !== undefined ? u.karmaScore : 0))));
|
||||
const kr = u.karmaReceived && typeof u.karmaReceived === 'object' ? u.karmaReceived : null;
|
||||
const krPos = kr ? Number(kr.positive) || 0 : 0;
|
||||
const krNeg = kr ? Number(kr.negative) || 0 : 0;
|
||||
const krTone = !kr ? '' : krPos > krNeg ? ' karma-value-positive' : krPos < krNeg ? ' karma-value-negative' : ' karma-value-neutral';
|
||||
if (show('karma')) items.push(span({ class: kr ? 'karma-line karma-line-stacked' : 'karma-line' },
|
||||
span({ class: 'karma-line-main' }, `${i18n.bankingUserEngagementScore}: `, strong({ class: 'karma-value' + krTone }, String(u.karmaScore !== undefined ? u.karmaScore : 0))),
|
||||
kr ? span({ class: 'karma-received-row' },
|
||||
span({ class: 'karma-received karma-received-up' }, span({ class: 'karma-arrow' }, '▲'), String(krPos)),
|
||||
span({ class: 'karma-received karma-received-down' }, span({ class: 'karma-arrow' }, '▼'), String(krNeg))
|
||||
) : null
|
||||
));
|
||||
if (show('ecoTax')) items.push(span({ class: 'karma-line eco-tax-line' }, `${i18n.profileVisibilityEcoTax || 'ECO Tax'}: `, strong(fmtCarbon(u.carbonGrams))));
|
||||
if (activityChip) items.push(activityChip);
|
||||
if (deviceChip) items.push(deviceChip);
|
||||
|
|
@ -3587,6 +3651,7 @@ exports.authorView = async ({
|
|||
relationship,
|
||||
ecoAddress,
|
||||
karmaScore = 0,
|
||||
karmaReceived = null,
|
||||
estimatedUBI = 0,
|
||||
lastClaimedDate = null,
|
||||
totalClaimed = 0,
|
||||
|
|
@ -3636,7 +3701,7 @@ exports.authorView = async ({
|
|||
|
||||
const userSensors = renderUserSensors({
|
||||
isMe: isOwnProfile, fediverseConfigured, prefs, id: feedId,
|
||||
karmaScore, carbonGrams, deviceSource, activityBucket: lastActivityBucket,
|
||||
karmaScore, karmaReceived, carbonGrams, deviceSource, activityBucket: lastActivityBucket,
|
||||
gpgFingerprint, ecoAddress, estimatedUBI, lastClaimedDate, totalClaimed,
|
||||
larpHouse, stats,
|
||||
mutual: !!(relationship && relationship.following && relationship.followsMe)
|
||||
|
|
@ -4818,7 +4883,10 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = '
|
|||
}
|
||||
|
||||
|
||||
exports.threadView = ({ messages, spreadMap = null }) => {
|
||||
exports.threadView = ({ messages, spreadMap = null, hiddenRoot = false }) => {
|
||||
if (!Array.isArray(messages) || !messages.length) {
|
||||
return template(i18n.hiddenByYou, section(p({ class: 'muted hidden-note' }, i18n.hiddenByYou)));
|
||||
}
|
||||
const rootMessage = messages[0];
|
||||
const rootAuthorName = rootMessage.value.meta.author.name;
|
||||
|
||||
|
|
@ -4829,6 +4897,7 @@ exports.threadView = ({ messages, spreadMap = null }) => {
|
|||
|
||||
const tpl = template(
|
||||
[`@${rootAuthorName}`],
|
||||
hiddenRoot ? p({ class: 'muted hidden-note' }, i18n.hiddenByYou) : null,
|
||||
div(thread(messages, spreadMap))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const RAW_HTML = Symbol.for('oasis.rawHtml');
|
|||
const { renderStyledText, renderStyledHtml, escapeHtml } = require("../backend/renderStyledText");
|
||||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
||||
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderIcon } = require("./main_views");
|
||||
const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderMapHtml, resolveView, makeView, fitView, parseView, viewParam, parsePick, MIN_ZOOM, MAX_ZOOM } = require("../maps/map_renderer");
|
||||
|
|
@ -37,8 +37,9 @@ const renderTags = (tags) => {
|
|||
let areaCounter = 0;
|
||||
const lastOf = (v) => (Array.isArray(v) ? v[v.length - 1] : v);
|
||||
const numOrNull = (v) => { const n = parseFloat(lastOf(v)); return isFinite(n) ? n : null; };
|
||||
const isBlob = (v) => !!v && String(v).startsWith("&");
|
||||
const blobSrc = (image, base) => (isBlob(image) ? `${base}/blob/${encodeURIComponent(image)}` : "");
|
||||
const isSealed = (v) => !!v && String(v).startsWith("/blob/sealed/");
|
||||
const isBlob = (v) => !!v && (String(v).startsWith("&") || isSealed(v));
|
||||
const blobSrc = (image, base) => (isSealed(image) ? (base ? "" : String(image)) : (isBlob(image) ? `${base}/blob/${encodeURIComponent(image)}` : ""));
|
||||
|
||||
const buildQuery = (params) => Object.entries(params || {})
|
||||
.filter(([, v]) => v !== undefined && v !== null && String(v) !== "")
|
||||
|
|
@ -174,7 +175,6 @@ const renderMapOwnerActions = (filter, mapObj, params = {}) => {
|
|||
const renderFilters = (filter, q, emptyMod = false, chip = null, search = null) =>
|
||||
div({ class: search ? "filters activity-filter-chips activity-toolbar-row" : "filters" },
|
||||
form({ method: "GET", action: "/maps", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterAll).toUpperCase()),
|
||||
...(!chip || chip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.mapFilterRecent).toUpperCase())] : []),
|
||||
|
|
@ -362,7 +362,7 @@ const renderPinPopup = (mk, i, ctx = {}) => {
|
|||
(ctx.centerHref ? `<a class="map-center-link" href="${escapeHtml(ctx.centerHref)}">${escapeHtml(i18n.mapCenterHere)}</a>` : "") +
|
||||
(own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) },
|
||||
ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null,
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, "✕")).outerHTML : "") + `</div>`
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))).outerHTML : "") + `</div>`
|
||||
];
|
||||
return parts.join("");
|
||||
};
|
||||
|
|
@ -393,7 +393,7 @@ const renderMarkersList = (markers, mapObj, ctx = {}) => {
|
|||
centerHref ? a({ href: centerHref, class: "map-center-link" }, i18n.mapCenterHere) : null,
|
||||
own ? form({ method: "POST", action: contentDeleteAction('mapMarker', mk.key) },
|
||||
ctx.returnTo ? input({ type: "hidden", name: "returnTo", value: ctx.returnTo }) : null,
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, "✕")) : null));
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.mapDeleteMarker }, renderIcon("close"))) : null));
|
||||
})));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -531,7 +531,6 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/market", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "minPrice", value: minPrice ?? "" }),
|
||||
input({ type: "hidden", name: "maxPrice", value: maxPrice ?? "" }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const detailHref = (type, key) => {
|
|||
case 'file': return `/files/${encodeURIComponent(key)}`;
|
||||
case 'document': return `/documents/${encodeURIComponent(key)}`;
|
||||
case 'feed': return `/feed/${encodeURIComponent(key)}`;
|
||||
case 'post': return `/blogs/${encodeURIComponent(key)}`;
|
||||
case 'votes': return `/votes/${encodeURIComponent(key)}`;
|
||||
case 'transfer': return `/transfers/${encodeURIComponent(key)}`;
|
||||
case 'industry': return `/industry/${encodeURIComponent(key)}`;
|
||||
|
|
@ -264,6 +265,16 @@ const renderContentHtml = (content, key) => {
|
|||
)
|
||||
);
|
||||
}
|
||||
case 'post':
|
||||
return div({ class: 'opinion-blog' },
|
||||
div({ class: 'card-section blog' },
|
||||
content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(key)}` }, content.contentWarning))) : "",
|
||||
div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text || '').trim().length > 420 ? String(content.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(content.text || '').trim())) }),
|
||||
Array.isArray(content.tags) && content.tags.length
|
||||
? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
|
||||
: null
|
||||
)
|
||||
);
|
||||
case 'feed':
|
||||
return div({ class: 'opinion-feed' },
|
||||
div({ class: 'card-section feed' },
|
||||
|
|
@ -392,12 +403,12 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems =
|
|||
{ class: 'trending-card opinions-card' + (isOwn ? ' own-content' : '') },
|
||||
div({ class: 'card-header activity-card-header' },
|
||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||
span({ class: 'pm-exposition-text' }, String(c.type || '').toUpperCase())
|
||||
span({ class: 'pm-exposition-text' }, String(c.type === 'post' ? (i18n.typePost || 'BLOG') : (c.type || '')).toUpperCase())
|
||||
),
|
||||
renderContentActions(key, detailHref(c.type, key), {
|
||||
spread: spreadMap.get(key) || null,
|
||||
author: item.value.author,
|
||||
reportTitle: c.title || c.question || c.concept || c.name || '',
|
||||
reportTitle: c.title || c.question || c.concept || c.name || c.contentWarning || '',
|
||||
...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&')
|
||||
? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } }
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false }) => {
|
||||
const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [], versions = {}, staleKeys = [], onionKeys = [], ownVersion = '', lastErrors = {}, paused = false, filter = 'status', advanced = false, health = null, lan = null }) => {
|
||||
const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea, wbr } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, renderStateChip } = require('./main_views');
|
||||
const { template, i18n, renderStateChip, renderModuleStats } = require('./main_views');
|
||||
const { NET_REASON_KEYS, canonicalKey } = require('../models/peer_health');
|
||||
const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0);
|
||||
const isOutdated = (v) => {
|
||||
|
|
@ -10,6 +10,8 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
};
|
||||
const stale = new Set(staleKeys || []);
|
||||
const outdatedKey = (k) => isOutdated((versions || {})[k]) || (!(versions || {})[k] && stale.has(k));
|
||||
const knowsVersion = (keys) => keys.some(k => (versions || {})[k] || stale.has(k));
|
||||
const unspecifiedHost = (h) => /^(0\.0\.0\.0|::|\[::\])$/.test(String(h || ''));
|
||||
const versionCell = (k) => {
|
||||
const v = (versions || {})[k];
|
||||
if (!v) return [stale.has(k) ? span({ title: i18n.peerVersionUnannounced }, renderStateChip('closed', null, String(i18n.peerOutdated).toUpperCase())) : '—'];
|
||||
|
|
@ -56,7 +58,7 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
};
|
||||
const onion = new Set(onionKeys || []);
|
||||
const overTor = (peerData) => /^onion:/i.test(String(peerData[0] || '')) || /\.onion$/i.test(String((peerData[1] || {}).host || '')) || onion.has(canonicalKey((peerData[1] || {}).key));
|
||||
const renderPeerRow = (peerData) => {
|
||||
const renderPeerRow = (peerData, showVersion) => {
|
||||
const peer = peerData[1];
|
||||
const { name, users, key } = peer;
|
||||
const tor = overTor(peerData);
|
||||
|
|
@ -64,9 +66,9 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
const filteredUsers = (users || []).filter(u => u.id !== key);
|
||||
const userCount = Array.isArray(users) ? filteredUsers.length : null;
|
||||
return tr(
|
||||
td({ 'data-label': i18n.peerHost || 'Pub' }, a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')),
|
||||
td({ 'data-label': i18n.peerHost || 'Pub' }, unspecifiedHost(name) ? '—' : a({ href: peerUrl, class: "user-link", title: name || key }, name ? shortHost(name) : key.slice(0, 20) + '…')),
|
||||
td({ 'data-label': i18n.peersOasisId || 'Oasis ID' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)),
|
||||
td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)),
|
||||
showVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(key)) : null,
|
||||
td({ 'data-label': i18n.peersTorColumn }, tor
|
||||
? span({ class: 'ubi-tick-ok', title: i18n.peersTorYes }, '✓')
|
||||
: span({ class: 'peer-tor-no', title: i18n.peersTorNo }, '—')),
|
||||
|
|
@ -84,18 +86,20 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
|
||||
const renderPeerTable = (peers, emptyKey) => {
|
||||
if (peers.length === 0) return p(i18n[emptyKey] || i18n.noConnections);
|
||||
const showVersion = knowsVersion(peers.map(pd => pd[1].key));
|
||||
return table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peerHost || 'Pub'),
|
||||
td({ class: 'card-label' }, i18n.peersOasisId || 'Oasis ID'),
|
||||
td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version'),
|
||||
showVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||
td({ class: 'card-label' }, i18n.peersTorColumn),
|
||||
td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated')
|
||||
),
|
||||
...peers.map(renderPeerRow)
|
||||
...peers.map(pd => renderPeerRow(pd, showVersion))
|
||||
);
|
||||
};
|
||||
|
||||
const techVersion = knowsVersion((technicalPeers || []).map(tp => tp.key));
|
||||
const technicalRows = (technicalPeers || []).map(tp => {
|
||||
const k = tp.key || '';
|
||||
const connected = tp.state === 'connected';
|
||||
|
|
@ -105,9 +109,9 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
td({ 'data-label': i18n.peersOasisId }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')),
|
||||
onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))
|
||||
? td({ 'data-label': i18n.peerAddressLabel, class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR'))
|
||||
: td({ 'data-label': i18n.peerAddressLabel, title: String(tp.host || ''), class: 'peer-host' }, tp.host ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'),
|
||||
: td({ 'data-label': i18n.peerAddressLabel, title: unspecifiedHost(tp.host) ? '' : String(tp.host || ''), class: 'peer-host' }, tp.host && !unspecifiedHost(tp.host) ? shortHost(tp.host).split(/(?<=\.)/).reduce((acc, part, i) => i ? [...acc, wbr(), part] : [part], []) : '—'),
|
||||
td({ 'data-label': i18n.peerPort, class: 'peer-port' }, String(tp.port || '—')),
|
||||
td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)),
|
||||
techVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(k)) : null,
|
||||
td({ 'data-label': i18n.peerStateLabel }, stateCell(tp)),
|
||||
td({ 'data-label': i18n.peerLastChangeLabel }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')),
|
||||
td(
|
||||
|
|
@ -120,31 +124,44 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
)
|
||||
);
|
||||
});
|
||||
const refreshButton = form({ action: "/peers/refresh", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh'));
|
||||
const pruneButton = form({ action: "/peers/prune", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle'));
|
||||
const exportButton = form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export'));
|
||||
const withCount = (label, n) => Number(n) > 0 ? `${label} (${n})` : label;
|
||||
const peersToolbar = (tabName, total, segments) => {
|
||||
if (!(Number(total) > 0)) return null;
|
||||
const back = input({ type: "hidden", name: "returnTo", value: `/peers?filter=${tabName}` });
|
||||
return div({ class: 'filters activity-filter-chips activity-toolbar-row' },
|
||||
renderModuleStats(total, segments),
|
||||
div({ class: "conn-actions peers-conn-actions filter-box" },
|
||||
form({ action: "/peers/refresh", method: "post" }, back, button({ type: "submit", class: "filter-btn" }, i18n.peerRefresh || 'Refresh')),
|
||||
form({ action: "/peers/prune", method: "post" }, back, tabName === 'lan' ? input({ type: "hidden", name: "scope", value: "lan" }) : null, button({ type: "submit", class: "filter-btn" }, i18n.peerPruneIdle || 'Remove idle')),
|
||||
form({ action: "/peers/export", method: "get" }, button({ type: "submit", class: "filter-btn" }, i18n.peerExport || 'Export'))
|
||||
)
|
||||
);
|
||||
};
|
||||
const importForm = form(
|
||||
{ action: "/peers/import", method: "post", enctype: "multipart/form-data", class: "peers-import-form" },
|
||||
label({ class: 'peers-import-label' }, i18n.peerImportTitle || 'Import peer list'),
|
||||
br(),
|
||||
textarea({ name: "peerList", rows: "4", placeholder: i18n.peerImportPlaceholder || 'Paste one multiserver address per line…' }),
|
||||
br(),
|
||||
input({ type: "file", name: "peerFile", accept: ".txt,text/plain" }),
|
||||
br(),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.peerImport || 'Import')
|
||||
);
|
||||
|
||||
const peersTechnicalBlock = div({ class: 'tags-header peers-technical-block' },
|
||||
h2(i18n.peerConnectionsTitle || 'Connections'),
|
||||
const peersTechnicalBlock = div({ class: 'peers-technical-block' },
|
||||
peersToolbar('connections', technicalPeers.length, [
|
||||
{ label: i18n.peerStateConnected, count: technicalPeers.filter(tp => tp.state === 'connected').length },
|
||||
{ label: i18n.peerOutdated, count: technicalPeers.filter(tp => outdatedKey(tp.key)).length },
|
||||
{ label: 'TOR', count: technicalPeers.filter(tp => onion.has(canonicalKey(tp.key)) || /\.onion$/i.test(String(tp.host || ''))).length }
|
||||
]),
|
||||
div({ class: "tags-header" }, h2(withCount(i18n.peerConnectionsTitle || 'Connections', technicalPeers.length)), p(i18n.peersConnectionsHint)),
|
||||
div({ class: "peers-pause-row" }, pauseButton, p({ class: paused ? "peers-paused-note" : "peers-pause-hint" }, paused ? i18n.peersPausedNote : i18n.peersPauseHint)),
|
||||
div({ class: "conn-actions peers-conn-actions filters" }, refreshButton, pruneButton, exportButton),
|
||||
technicalPeers.length
|
||||
? table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peersOasisId),
|
||||
td({ class: 'card-label' }, i18n.peerAddressLabel),
|
||||
td({ class: 'card-label peer-port' }, i18n.peerPort),
|
||||
td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version'),
|
||||
techVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||
td({ class: 'card-label' }, i18n.peerLastChangeLabel),
|
||||
td({ class: 'card-label' }, '')
|
||||
|
|
@ -155,6 +172,121 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
importForm
|
||||
);
|
||||
|
||||
const fmtDate = (ts) => ts ? new Date(ts).toISOString().slice(0, 16).replace('T', ' ') : '—';
|
||||
const healthRows = health && Array.isArray(health.rows) ? health.rows : [];
|
||||
const healthKind = (r) => {
|
||||
if (r.state === 'connected') return 'connected';
|
||||
if (r.dead) return 'unreachable';
|
||||
if ((Number(r.failures) || 0) > 0 || r.reason) return 'failing';
|
||||
return 'reachable';
|
||||
};
|
||||
const healthChip = (r) => {
|
||||
const k = healthKind(r);
|
||||
if (k === 'connected') return renderStateChip('mutuals', null, String(i18n.peerStateConnected).toUpperCase());
|
||||
if (k === 'unreachable') return renderStateChip('closed', null, String(i18n.graphosFedUnreachable).toUpperCase());
|
||||
if (k === 'failing') return renderStateChip('hidden', null, String(i18n[NET_REASON_KEYS[r.reason] || 'peerErrSilent'] || i18n.peerErrOther).toUpperCase());
|
||||
return renderStateChip(outdatedKey(r.key) ? 'whole' : 'mutuals', null, String(i18n.peersHealthReachable).toUpperCase());
|
||||
};
|
||||
const healthCount = (k) => healthRows.filter(r => healthKind(r) === k).length;
|
||||
const deadCount = healthRows.filter(r => r.dead).length;
|
||||
const healthVersion = knowsVersion(healthRows.map(r => r.key));
|
||||
const healthBlock = !healthRows.length ? div({ class: 'peers-list' }, p(i18n.noConnections)) : div({ class: 'peers-technical-block peers-health-block' },
|
||||
peersToolbar('health', healthRows.length, [
|
||||
{ label: i18n.peersHealthReachable, count: healthCount('connected') + healthCount('reachable') },
|
||||
{ label: i18n.peersHealthFailing, count: healthCount('failing') },
|
||||
{ label: i18n.graphosFedUnreachable, count: deadCount },
|
||||
{ label: i18n.peerOutdated, count: healthRows.filter(r => outdatedKey(r.key)).length },
|
||||
{ label: 'TOR', count: healthRows.filter(r => r.tor).length }
|
||||
]),
|
||||
div({ class: "tags-header" }, h2(withCount(i18n.peersTabHealth, healthRows.length)), p(i18n.peersHealthHint)),
|
||||
deadCount
|
||||
? div({ class: "conn-actions peers-conn-actions filters" },
|
||||
form({ action: "/peers/clear-unreachable", method: "post" }, button({ type: "submit", class: "filter-btn" }, i18n.peersHealthForget)))
|
||||
: null,
|
||||
healthRows.length
|
||||
? table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peerHost || 'IP'),
|
||||
td({ class: 'card-label' }, i18n.peersOasisId || 'ID'),
|
||||
td({ class: 'card-label' }, i18n.graphosFedTransport),
|
||||
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||
td({ class: 'card-label' }, i18n.peersHealthFailures),
|
||||
td({ class: 'card-label' }, i18n.peersFailingSince),
|
||||
healthVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null
|
||||
),
|
||||
...healthRows.map(r => tr(
|
||||
r.tor
|
||||
? td({ 'data-label': i18n.peerHost || 'IP', class: 'peer-host' }, renderStateChip('fediverse', null, 'TOR'))
|
||||
: td({ 'data-label': i18n.peerHost || 'IP', title: unspecifiedHost(r.host) ? '' : String(r.host || ''), class: 'peer-host' }, r.host && !unspecifiedHost(r.host) ? shortHost(r.host) : '—'),
|
||||
td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)),
|
||||
td({ 'data-label': i18n.graphosFedTransport }, r.tor ? renderStateChip('fediverse', null, 'TOR') : renderStateChip('transport', null, String(i18n.graphosFedClearnet).toUpperCase())),
|
||||
td({ 'data-label': i18n.peerStateLabel }, healthChip(r)),
|
||||
td({ 'data-label': i18n.peersHealthFailures }, String(Number(r.failures) || 0)),
|
||||
td({ 'data-label': i18n.peersFailingSince }, r.failingSince ? fmtDate(r.failingSince) : '—'),
|
||||
healthVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null
|
||||
))
|
||||
)
|
||||
: p(i18n.noConnections)
|
||||
);
|
||||
|
||||
const lanRows = lan && Array.isArray(lan.rows) ? lan.rows : [];
|
||||
const lanOn = !!(lan && lan.broadcasting);
|
||||
const lanVersion = knowsVersion(lanRows.map(r => r.key));
|
||||
const lanBlock = div({ class: 'peers-technical-block peers-lan-block' },
|
||||
peersToolbar('lan', lanRows.length, [
|
||||
{ label: i18n.online, count: lanRows.filter(r => r.online).length },
|
||||
{ label: i18n.peerOutdated, count: lanRows.filter(r => outdatedKey(r.key)).length }
|
||||
]),
|
||||
div({ class: "tags-header" }, h2(withCount(i18n.peersTabLan, lanRows.length)), p(i18n.peersLanHint)),
|
||||
div({ class: "peers-pause-row" },
|
||||
p({ class: "peers-pause-hint" }, `${i18n.settingsLanTitle}: `, renderStateChip(lanOn ? 'mutuals' : 'hidden', null, String(lanOn ? i18n.modulesEnabledModulesLabel : i18n.modulesDisabledModulesLabel).toUpperCase()))
|
||||
),
|
||||
lanRows.length
|
||||
? table({ class: 'block-info-table' },
|
||||
tr(
|
||||
td({ class: 'card-label' }, i18n.peersOasisId || 'ID'),
|
||||
td({ class: 'card-label' }, i18n.peerStateLabel),
|
||||
td({ class: 'card-label' }, i18n.peersLanLastSeen),
|
||||
lanVersion ? td({ class: 'card-label' }, i18n.peersOasisVersion || 'Version') : null,
|
||||
td({ class: 'card-label' }, '')
|
||||
),
|
||||
...lanRows.map(r => tr(
|
||||
td({ 'data-label': i18n.peersOasisId || 'ID' }, a({ href: `/author/${encodeURIComponent(r.key)}`, class: 'user-link peer-key', title: r.name || r.key }, r.key)),
|
||||
td({ 'data-label': i18n.peerStateLabel }, r.online ? renderStateChip('mutuals', null, String(i18n.online).toUpperCase()) : renderStateChip('hidden', null, String(i18n.offline).toUpperCase())),
|
||||
td({ 'data-label': i18n.peersLanLastSeen }, fmtDate(r.seenAt)),
|
||||
lanVersion ? td({ 'data-label': i18n.peersOasisVersion || 'Version' }, ...versionCell(r.key)) : null,
|
||||
td(r.online ? null : form({ method: "POST", action: "/peers/lan/forget", class: "inline-form" },
|
||||
input({ type: "hidden", name: "key", value: r.key }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.delete)
|
||||
))
|
||||
))
|
||||
)
|
||||
: p(i18n.peersLanEmpty)
|
||||
);
|
||||
|
||||
const tabs = ['status', 'connections'].concat(advanced ? ['lan', 'health'] : []);
|
||||
const tab = tabs.includes(filter) ? filter : 'status';
|
||||
const tabLabel = { status: i18n.status || 'Status', connections: i18n.peerConnectionsTitle || 'Connections', lan: i18n.peersTabLan, health: i18n.peersTabHealth };
|
||||
const statusToolbar = peersToolbar('status', onlineCount + discoveredCount + unknownCount, [
|
||||
{ label: i18n.online, count: onlineCount },
|
||||
{ label: i18n.discovered, count: discoveredCount },
|
||||
{ label: i18n.unknown, count: unknownCount }
|
||||
]);
|
||||
const statusBlock = (onlineCount + discoveredCount + unknownCount) > 0
|
||||
? div({ class: "peers-list" },
|
||||
statusToolbar,
|
||||
...[
|
||||
[i18n.online, onlineCount, dedupOnline, 'noConnections', i18n.peersOnlineHint],
|
||||
[i18n.discovered, discoveredCount, dedupDiscovered, 'noDiscovered', i18n.peersDiscoveredHint],
|
||||
[i18n.unknown, unknownCount, dedupUnknown, 'noUnknownPeers', i18n.peersUnknownHint]
|
||||
].filter(([, count]) => count > 0).flatMap(([title, count, list, emptyKey, hint], idx) => [
|
||||
idx ? hr() : null,
|
||||
div({ class: "tags-header" }, h2(withCount(title, count)), p(hint)),
|
||||
renderPeerTable(list, emptyKey)
|
||||
])
|
||||
)
|
||||
: div({ class: "peers-list" }, statusToolbar, p(i18n.noConnections));
|
||||
|
||||
return template(
|
||||
i18n.peers,
|
||||
section(
|
||||
|
|
@ -162,20 +294,14 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc
|
|||
h2(i18n.peers),
|
||||
p(i18n.peerConnectionsIntro)
|
||||
),
|
||||
(onlineCount + discoveredCount + unknownCount) > 0
|
||||
? div({ class: "peers-list" },
|
||||
div({ class: "tags-header" }, h2(`${i18n.online} (${onlineCount})`)),
|
||||
renderPeerTable(dedupOnline, 'noConnections'),
|
||||
hr(),
|
||||
div({ class: "tags-header" }, h2(`${i18n.discovered} (${discoveredCount})`)),
|
||||
renderPeerTable(dedupDiscovered, 'noDiscovered'),
|
||||
hr(),
|
||||
div({ class: "tags-header" }, h2(`${i18n.unknown} (${unknownCount})`)),
|
||||
renderPeerTable(dedupUnknown, 'noUnknownPeers')
|
||||
)
|
||||
: null,
|
||||
peersTechnicalBlock,
|
||||
p(i18n.connectionActionIntro)
|
||||
div({ class: 'mode-buttons-row' },
|
||||
...tabs.map(t => a({ class: tab === t ? 'filter-btn active' : 'filter-btn', href: `/peers?filter=${t}` }, String(tabLabel[t]).toUpperCase()))
|
||||
),
|
||||
tab === 'status' ? statusBlock : null,
|
||||
tab === 'connections' ? peersTechnicalBlock : null,
|
||||
tab === 'connections' ? p(i18n.connectionActionIntro) : null,
|
||||
tab === 'lan' ? lanBlock : null,
|
||||
tab === 'health' ? healthBlock : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
const { div, h2, p, section, button, form, input, span, audio, table, tr, td, th, br, a } = require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n, userLink, userLinkLabel, renderCallButton, liveClock, renderStateChip, renderVideoGrid } = require("./main_views");
|
||||
const { template, i18n, userLink, userLinkLabel, renderCallButton, liveClock, renderStateChip, renderVideoGrid, renderIcon } = require("./main_views");
|
||||
const { renderEncryptedChip } = require("./clearnet_view");
|
||||
const { phoneNumberOf } = require("../models/phone_number");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
|
||||
const PAM_MAX_MS = require('../server/phone_module').VOICEMAIL_MAX_MS;
|
||||
const GROUP_MAX = require('../server/phone_module').GROUP_MAX;
|
||||
|
||||
const clock = (ms) => {
|
||||
const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
|
||||
|
|
@ -105,7 +106,7 @@ const NUMBER_TOKEN = /^\d{3}-\d{3}$/;
|
|||
|
||||
const renderCompose = (compose) => {
|
||||
const recipients = Array.isArray(compose.recipients) ? compose.recipients : [];
|
||||
const max = Number(compose.max) || 7;
|
||||
const max = Number(compose.max) || GROUP_MAX;
|
||||
const dial = String(compose.dial || '');
|
||||
const full = recipients.length >= max;
|
||||
const keep = () => [input({ type: 'hidden', name: 'filter', value: 'create' }), input({ type: 'hidden', name: 'to', value: compose.to || '' })];
|
||||
|
|
@ -127,7 +128,7 @@ const renderCompose = (compose) => {
|
|||
key('clear', '✕', 'phone-key phone-key-fn', { title: i18n.phoneDialClear })
|
||||
)
|
||||
),
|
||||
p({ class: 'phone-empty phone-dial-hint' }, i18n.phoneLimitsHint),
|
||||
p({ class: 'phone-empty phone-dial-hint' }, String(i18n.phoneLimitsHint).replace('{n}', String(max))),
|
||||
div({ class: 'phone-recipients' },
|
||||
span({ class: 'card-label' }, `${i18n.phoneDialInhabitants}: ${recipients.length}/${max}`),
|
||||
recipients.length ? div({ class: 'phone-recipient-list' },
|
||||
|
|
@ -183,7 +184,7 @@ const renderRecords = (pams) =>
|
|||
const deleteCalls = (ids) => ids.length
|
||||
? form({ method: 'POST', action: '/phone/delete-shown', class: 'content-action-form phone-row-delete' },
|
||||
...ids.map(id => input({ type: 'hidden', name: 'calls', value: id })),
|
||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.phoneDelete }, '✕')
|
||||
button({ type: 'submit', class: 'btn-singleview btn-delete', title: i18n.phoneDelete }, renderIcon('close'))
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ const renderFilters = (filter, q, census, emptyMod) =>
|
|||
section(
|
||||
div({ class: "filters" },
|
||||
form({ method: "GET", action: "/podcasts", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
...(emptyMod ? [] : [
|
||||
...chipButtons(BASE_FILTERS, filter, census, filterLabel),
|
||||
...chipButtons(CATEGORIES, filter, census, catLabel)
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ exports.singlePollView = async (poll, comments = [], params = {}) => {
|
|||
: (poll.voters.length
|
||||
? div({ class: "card-assigned-list" }, ...poll.voters.map(v => userLink(v)))
|
||||
: null),
|
||||
renderEngagement(poll.id,
|
||||
poll.tribeId ? null : renderEngagement(poll.id,
|
||||
renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants),
|
||||
renderComments(poll, comments, basePath)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { div, h2, p, section, button, form, a, span, br, hr, label, textarea, input, select, option, img, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe")
|
||||
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed } = require("./main_views")
|
||||
const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed, renderIcon } = require("./main_views")
|
||||
const { renderEncryptedChip, renderTransportChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view")
|
||||
const { renderStyledText } = require("../backend/renderStyledText")
|
||||
const moment = require("../server/node_modules/moment")
|
||||
|
|
@ -68,7 +68,7 @@ const renderModeButtons = (currentFilter, emptyMod = false, modesAvail = null) =
|
|||
)
|
||||
)
|
||||
|
||||
const renderCover = (room, cls) => room.image ? img({ loading: "lazy", src: `/blob/${encodeURIComponent(room.image)}`, class: cls, alt: "" }) : null
|
||||
const renderCover = (room, cls) => room.image ? img({ loading: "lazy", src: String(room.image).startsWith("/blob/sealed/") ? room.image : `/blob/${encodeURIComponent(room.image)}`, class: cls, alt: "" }) : null
|
||||
|
||||
const roomIsFull = (occ) => !!occ && occ.count >= ((occ && occ.max) || ROOM_MAX)
|
||||
|
||||
|
|
@ -194,18 +194,6 @@ const renderRecChip = () => span({ class: "room-rec-chip" }, "● " + String(i18
|
|||
const fmtDuration = (ms) => { const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}` }
|
||||
const refreshLink = (room) => a({ href: roomHref(room), class: "tribe-action-btn room-refresh-btn" }, String(i18n.liveRefresh).toUpperCase())
|
||||
const renderLiveControls = (room, live) => div({ class: "room-live-controls" },
|
||||
form({ method: "POST", action: "/rooms/notify", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "on", value: live.notify ? "0" : "1" }),
|
||||
button({ type: "submit", class: live.notify ? "tribe-action-btn" : "tribe-action-btn room-notify-off" }, "🔔 " + String(live.notify ? i18n.roomNotificationsOn : i18n.roomNotificationsOff).toUpperCase())
|
||||
),
|
||||
form({ method: "POST", action: "/rooms/hand", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "hand", value: live.hand ? "0" : "1" }),
|
||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||
button({ type: "submit", class: live.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn" }, "✋ " + String(live.hand ? i18n.roomLowerHand : i18n.roomRaiseHand).toUpperCase())
|
||||
),
|
||||
live.notify && safeArr(live.events).length
|
||||
? form({ method: "POST", action: "/rooms/notify/clear", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomClearNotices).toUpperCase()))
|
||||
: null,
|
||||
form({ method: "POST", action: "/rooms/mute", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "mute", value: live.muted ? "0" : "1" }),
|
||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||
|
|
@ -214,14 +202,40 @@ const renderLiveControls = (room, live) => div({ class: "room-live-controls" },
|
|||
live.videoAvailable
|
||||
? form({ method: "POST", action: "/rooms/camera", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "cam", value: live.cam ? "0" : "1" }),
|
||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||
input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }),
|
||||
button({ type: "submit", class: live.cam ? "tribe-action-btn room-cam-btn-on" : "tribe-action-btn" }, String(live.cam ? i18n.phoneCameraOff : i18n.phoneCameraOn).toUpperCase())
|
||||
)
|
||||
: null,
|
||||
live.streaming != null
|
||||
? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "stop", value: "1" }),
|
||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||
button({ type: "submit", class: "tribe-action-btn room-cam-btn-on" }, String(i18n.roomStreamStop).toUpperCase())
|
||||
)
|
||||
: live.streamAvailable
|
||||
? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form room-stream-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }),
|
||||
input({ type: "file", id: "room-stream-file", name: "video", accept: "video/*", required: true, class: "room-stream-input" }),
|
||||
label({ for: "room-stream-file", class: "tribe-action-btn room-stream-pick" }, String(i18n.roomStreamFile).toUpperCase()),
|
||||
button({ type: "submit", class: "tribe-action-btn room-cam-btn-on room-stream-go", title: i18n.roomStreamGo }, "▶")
|
||||
)
|
||||
: null,
|
||||
form({ method: "POST", action: "/rooms/hand", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "hand", value: live.hand ? "0" : "1" }),
|
||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||
button({ type: "submit", class: live.hand ? "tribe-action-btn room-hand-btn-on" : "tribe-action-btn" }, "✋ " + String(live.hand ? i18n.roomLowerHand : i18n.roomRaiseHand).toUpperCase())
|
||||
),
|
||||
live.recording
|
||||
? form({ method: "POST", action: "/rooms/rec/stop", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.roomRecStop).toUpperCase()))
|
||||
: form({ method: "POST", action: "/rooms/rec/start", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomRec).toUpperCase())),
|
||||
live.recording || safeArr(live.recordingBy).length ? renderRecChip() : null,
|
||||
form({ method: "POST", action: "/rooms/notify", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "on", value: live.notify ? "0" : "1" }),
|
||||
button({ type: "submit", class: live.notify ? "tribe-action-btn" : "tribe-action-btn room-notify-off" }, "🔔 " + String(live.notify ? i18n.roomNotificationsOn : i18n.roomNotificationsOff).toUpperCase())
|
||||
),
|
||||
live.notify && safeArr(live.events).length
|
||||
? form({ method: "POST", action: "/rooms/notify/clear", class: "phone-action-form" }, button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomClearNotices).toUpperCase()))
|
||||
: null,
|
||||
refreshLink(room),
|
||||
form({ method: "POST", action: "/rooms/leave", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "returnTo", value: roomHref(room) }),
|
||||
|
|
@ -245,7 +259,7 @@ const renderRecordings = (recordings) => safeArr(recordings).length
|
|||
span({ class: "phone-call-clock" }, fmtDuration(r.durationMs)),
|
||||
a({ href: `/rooms/recordings/${encodeURIComponent(r.name)}`, class: "tribe-action-btn", download: r.name }, String(i18n.fileDownload).toUpperCase()),
|
||||
form({ method: "POST", action: `/rooms/recordings/${encodeURIComponent(r.name)}/delete`, class: "phone-action-form" },
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.delete }, "✕"))
|
||||
button({ type: "submit", class: "btn-singleview btn-delete", title: i18n.delete }, renderIcon("close")))
|
||||
)))
|
||||
)
|
||||
: null
|
||||
|
|
@ -260,34 +274,44 @@ const renderLivePanel = (room, live, occ, canJoin = false) => {
|
|||
input({ type: "hidden", name: "on", value: on ? "0" : "1" }),
|
||||
button({ type: "submit", class: on ? "tribe-action-btn danger-btn" : "tribe-action-btn" }, String(on ? i18n.phoneUnsilence : i18n.phoneSilence).toUpperCase())
|
||||
)
|
||||
const who = (id, muted, you, silenced = false, recording = false, turn = 0, cam = false) => div({ class: "phone-contact room-peer" + (turn ? " room-peer-raised" : "") },
|
||||
const isPublic = !!live.public || (room.clearnet === true && clearnetEligible(room))
|
||||
const who = (id, muted, you, silenced = false, recording = false, turn = 0, cam = false, film = false, viewer = false) => viewer
|
||||
? div({ class: "phone-contact room-peer" }, userLink(id), renderReachChip(true, i18n))
|
||||
: div({ class: "phone-contact room-peer" + (turn ? " room-peer-raised" : "") },
|
||||
turn ? span({ class: "room-peer-hand" }, `✋ ${turn}`) : null,
|
||||
userLink(id),
|
||||
you ? span({ class: "room-peer-you" }, i18n.roomYou) : null,
|
||||
muted ? span({ class: "room-peer-muted" }, String(i18n.roomMuted).toUpperCase()) : null,
|
||||
cam ? span({ class: "room-peer-cam" }, "CAM") : null,
|
||||
film ? span({ class: "room-peer-cam" }, "▶") : null,
|
||||
recording ? renderRecChip() : null,
|
||||
you ? null : silenceForm(id, silenced)
|
||||
)
|
||||
return div({ class: "phone-call-panel room-live-panel" },
|
||||
head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null), renderLiveControls(room, live)),
|
||||
head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), isPublic ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null, live.streaming != null ? span({ class: "room-stream-chip", title: live.streaming || "" }, "▶ " + String(live.streaming || i18n.roomStreaming)) : null), renderLiveControls(room, live)),
|
||||
live.grid
|
||||
? (safeArr(live.gridOrder).length
|
||||
? div({ class: "video-grid video-grid-one" },
|
||||
? div({ id: "room-video", class: safeArr(live.gridOrder).length === 1 ? "video-grid video-grid-one video-grid-solo" : "video-grid video-grid-one" },
|
||||
...renderLiveFeed("/phone/video/grid", "video-feed video-feed-grid"),
|
||||
div({ class: "video-grid-names" }, ...safeArr(live.gridOrder).map(id => id === userId ? span({ class: "video-tile-name" }, i18n.roomYou) : userLink(id)))
|
||||
div({ class: "video-grid-names" }, ...safeArr(live.gridOrder).map(key => {
|
||||
const film = String(key).startsWith("film:")
|
||||
const id = film ? String(key).slice(5) : key
|
||||
const name = id === userId ? i18n.roomYou : userLink(id)
|
||||
return film ? span({ class: "video-tile-name video-tile-film" }, "▶ ", name) : (id === userId ? span({ class: "video-tile-name" }, name) : name)
|
||||
}))
|
||||
)
|
||||
: null)
|
||||
: renderVideoGrid(safeArr(live.peers).map(p => ({ id: p.id, on: !!p.cam })), !!live.cam),
|
||||
: div({ id: "room-video" }, renderVideoGrid(safeArr(live.peers).map(p => ({ id: p.id, on: !!p.cam, film: !!p.film })), !!live.cam, { film: live.streaming != null })),
|
||||
div({ class: "phone-contacts" }, ...handOrder(live).map(e => e.you
|
||||
? who(userId, live.muted, true, false, !!live.recording, e.turn, !!live.cam)
|
||||
: who(e.peer.id, e.peer.muted, false, !!e.peer.silenced, !!e.peer.recording, e.turn, !!e.peer.cam))),
|
||||
? who(userId, live.muted, true, false, !!live.recording, e.turn, !!live.cam, live.streaming != null)
|
||||
: who(e.peer.id, e.peer.muted, false, !!e.peer.silenced, !!e.peer.recording, e.turn, !!e.peer.cam, !!e.peer.film, !!e.peer.viewer))),
|
||||
renderRoomEvents(live)
|
||||
)
|
||||
}
|
||||
const count = occ ? occ.count : 0
|
||||
return div({ class: "phone-block room-live-panel" },
|
||||
head(p({ class: "phone-empty" }, count > 0 ? String(i18n.roomSomeone).replace("{n}", String(count)) : i18n.roomNobody), canJoin ? div({ class: "room-live-controls" }, refreshLink(room), joinForm(room, occ)) : null)
|
||||
head(p({ class: "phone-empty" }, count > 0 ? String(i18n.roomSomeone).replace("{n}", String(count)) : i18n.roomNobody), canJoin ? div({ class: "room-live-controls" }, refreshLink(room), joinForm(room, occ)) : null),
|
||||
canJoin && room.clearnet === true && clearnetEligible(room) ? p({ class: "room-public-warning" }, i18n.roomPublicWarning) : null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -327,9 +351,13 @@ exports.singleRoomView = async (room, params = {}) => {
|
|||
|
||||
const ownerActions = isAuthor
|
||||
? [
|
||||
room.isClosed ? null : form({ method: "POST", action: `/rooms/close/${encodeURIComponent(room.rootId)}` },
|
||||
button({ type: "submit", class: "update-btn" }, i18n.roomClose)
|
||||
),
|
||||
room.isClosed
|
||||
? form({ method: "POST", action: `/rooms/reopen/${encodeURIComponent(room.rootId)}` },
|
||||
button({ type: "submit", class: "update-btn" }, i18n.roomReopen)
|
||||
)
|
||||
: form({ method: "POST", action: `/rooms/close/${encodeURIComponent(room.rootId)}` },
|
||||
button({ type: "submit", class: "update-btn delete-btn" }, i18n.roomClose)
|
||||
),
|
||||
form({ method: "GET", action: "/rooms" },
|
||||
input({ type: "hidden", name: "filter", value: "edit" }),
|
||||
input({ type: "hidden", name: "id", value: room.rootId }),
|
||||
|
|
@ -423,8 +451,8 @@ exports.renderTribeRoomsSection = (tribe, rooms, occupancy, live, toolbar = null
|
|||
exports.renderRoomItem = renderRoomItem
|
||||
exports.roomStatsBy = (rooms) => renderModuleStatsBy(safeArr(rooms), r => r.isClosed ? "CLOSED" : r.type, [{ value: "OPEN", label: i18n.roomStatusOpen }, { value: "INVITE-ONLY", label: i18n.roomStatusInviteOnly }, { value: "CLOSED", label: i18n.roomStatusClosed }])
|
||||
|
||||
exports.clearnetRoomView = async (room) => {
|
||||
const { escapeHtml: esc, renderKindTag, renderClearnetPage } = require("./clearnet_view")
|
||||
exports.clearnetRoomView = async (room, { live = false, sound = true, base = "" } = {}) => {
|
||||
const { escapeHtml: esc, renderKindTag, renderClearnetPage, cnZoom } = require("./clearnet_view")
|
||||
const name = room.title || i18n.cnKindRoom
|
||||
const extraCss = `
|
||||
.cn-room-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word}
|
||||
|
|
@ -432,6 +460,13 @@ exports.clearnetRoomView = async (room) => {
|
|||
.cn-room-number{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--fg);border-radius:10px;padding:4px 12px;font-size:16px;font-weight:600;color:var(--fg);background:var(--bg-sub)}
|
||||
.cn-room-number .pm-exposition-icon{font-size:18px;line-height:1}
|
||||
.cn-room-number .pm-exposition-text{line-height:1;letter-spacing:1px;font-family:monospace}
|
||||
.cn-room-live{display:flex;flex-direction:column;align-items:center;gap:12px;margin-top:24px}
|
||||
.cn-room-live-chip{align-self:flex-start;border:1px solid #2ecc71;border-radius:10px;padding:3px 10px;color:#2ecc71;font-size:13px;font-weight:700;letter-spacing:1px}
|
||||
.cn-room-live .cn-zoom{display:block;width:100%;max-width:960px}
|
||||
.cn-room-grid{width:100%;aspect-ratio:4/3;object-fit:contain;background:#000;border-radius:8px}
|
||||
.cn-room-sound{width:100%;max-width:960px}
|
||||
.cn-room-live .cn-lightbox-img{width:92vw;height:88vh;object-fit:contain}
|
||||
.cn-room-live .cn-lightbox-full .cn-lightbox-img{width:100vw;height:100vh}
|
||||
`
|
||||
const body = `
|
||||
<h1 class="cn-room-title">${esc(name)}</h1>
|
||||
|
|
@ -439,6 +474,11 @@ exports.clearnetRoomView = async (room) => {
|
|||
${renderKindTag("room")}
|
||||
${room.number ? `<span class="pm-exposition-chip pm-exposition-whole cn-room-number"><span class="pm-exposition-icon">✆</span><span class="pm-exposition-text">${esc(room.number)}</span></span>` : ""}
|
||||
</div>
|
||||
${live && base ? `<section class="cn-room-live">
|
||||
<span class="cn-room-live-chip">● ${esc(String(i18n.roomLiveChip || "LIVE").toUpperCase())}</span>
|
||||
${cnZoom("room-live", `${base}/live`, name, { full: true, imgClass: "cn-room-grid" })}
|
||||
${sound ? `<audio class="cn-room-sound" controls preload="none" src="${esc(`${base}/sound`)}"></audio>` : ""}
|
||||
</section>` : ""}
|
||||
`
|
||||
return renderClearnetPage({
|
||||
title: `${name} | Oasis`,
|
||||
|
|
@ -461,3 +501,26 @@ exports.roomModesFromCensus = (census, me, occupancy) => {
|
|||
closed: list.some(x => x.isClosed)
|
||||
}
|
||||
}
|
||||
|
||||
const pickerSize = (bytes) => {
|
||||
const n = Number(bytes) || 0
|
||||
if (n >= 1073741824) return `${(n / 1073741824).toFixed(1)} GB`
|
||||
if (n >= 1048576) return `${(n / 1048576).toFixed(1)} MB`
|
||||
return `${Math.max(1, Math.round(n / 1024))} KB`
|
||||
}
|
||||
|
||||
exports.roomStreamMatchesView = ({ files, returnTo }) => template(
|
||||
i18n.roomStreamPickTitle,
|
||||
section(
|
||||
div({ class: "tags-header module-header-line" }, h2(i18n.roomStreamPickTitle), p(i18n.roomStreamPickHint)),
|
||||
div({ class: "mode-buttons-row" }, a({ href: returnTo, class: "filter-btn" }, String(i18n.phoneCancel).toUpperCase())),
|
||||
ul({ class: "stream-picker-list" }, ...safeArr(files).map(f => li({ class: "stream-picker-item" },
|
||||
span({ class: "stream-picker-link" }, renderIcon("film"), span({ class: "stream-picker-name" }, f.path), span({ class: "stream-picker-size" }, pickerSize(f.size))),
|
||||
form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" },
|
||||
input({ type: "hidden", name: "file", value: f.path }),
|
||||
input({ type: "hidden", name: "returnTo", value: returnTo }),
|
||||
button({ type: "submit", class: "tribe-action-btn" }, String(i18n.roomStreamFile).toUpperCase())
|
||||
)
|
||||
)))
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe");
|
||||
const RAW_HTML = Symbol.for('oasis.rawHtml');
|
||||
const { template, i18n, userLink, renderContentActions, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel } = require('./main_views');
|
||||
const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel } = require('./main_views');
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText');
|
||||
const { renderStyledText } = require('../backend/renderStyledText');
|
||||
|
|
@ -18,16 +18,51 @@ const industryStatusLabel = (status) => {
|
|||
};
|
||||
|
||||
|
||||
const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, spreadMap = null, favIndex = null }) => {
|
||||
const searchInput = input({
|
||||
id: "search_query",
|
||||
name: "query",
|
||||
required: false,
|
||||
type: "search",
|
||||
value: query,
|
||||
placeholder: i18n.searchPlaceholder
|
||||
});
|
||||
const renderSearchForm = ({ action, query = "", queryName = "query", hidden = [] } = {}) => {
|
||||
const searchInput = input({ id: "search_query", name: queryName, required: false, type: "search", value: query, placeholder: i18n.searchPlaceholder });
|
||||
searchInput.setAttribute("minlength", 3);
|
||||
const resultCount = currentPerPage();
|
||||
const resultsPerPageSelect = select(
|
||||
{ id: "results-per-page", name: "perPage", class: "input-select search-select" },
|
||||
LIST_PAGE_SIZES.map(s => option({ value: s, selected: resultCount === s ? "selected" : undefined }, pageSizeLabel(s)))
|
||||
);
|
||||
return form(
|
||||
{ action, method: "GET", class: "search-form" },
|
||||
...hidden.map(([name, value]) => input({ type: "hidden", name, value })),
|
||||
div({ class: "search-filters-row" },
|
||||
table({ class: "search-filters-table" },
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")),
|
||||
td(input({ id: "search_from", type: "datetime-local", name: "from" }))
|
||||
),
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")),
|
||||
td(input({ id: "search_to", type: "datetime-local", name: "to" }))
|
||||
),
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")),
|
||||
td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" }))
|
||||
)
|
||||
),
|
||||
table({ class: "search-filters-table" },
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")),
|
||||
td(searchInput)
|
||||
),
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")),
|
||||
td(resultsPerPageSelect)
|
||||
)
|
||||
)
|
||||
),
|
||||
div({ class: "search-submit-row" },
|
||||
button({ type: "submit" }, i18n.searchSubmit)
|
||||
)
|
||||
);
|
||||
};
|
||||
exports.renderSearchForm = renderSearchForm;
|
||||
|
||||
const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = [], hashtag = null, results = {}, spreadMap = null, favIndex = null }) => {
|
||||
|
||||
const contentTypes = [
|
||||
"post", "about", "curriculum", "tribe", "market", "transfer", "feed", "votes",
|
||||
|
|
@ -49,15 +84,6 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
)
|
||||
);
|
||||
|
||||
const resultCount = currentPerPage();
|
||||
const resultsPerPageSelect = select(
|
||||
{
|
||||
id: "results-per-page",
|
||||
name: "perPage",
|
||||
class: "input-select search-select"
|
||||
},
|
||||
LIST_PAGE_SIZES.map(s => option({ value: s, selected: resultCount === s ? "selected" : undefined }, pageSizeLabel(s)))
|
||||
);
|
||||
|
||||
const getViewDetailsActionForSearch = (type, contentId, content) => {
|
||||
switch (type) {
|
||||
|
|
@ -75,7 +101,9 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
case 'bookmark': return `/bookmarks/${encodeURIComponent(contentId)}`;
|
||||
case 'event': return `/events/${encodeURIComponent(contentId)}`;
|
||||
case 'task': return `/tasks/${encodeURIComponent(contentId)}`;
|
||||
case 'post': return `/thread/${encodeURIComponent(contentId)}#${encodeURIComponent(contentId)}`;
|
||||
case 'post': return content && typeof content.root === 'string'
|
||||
? `/thread/${encodeURIComponent(content.root)}#${encodeURIComponent(contentId)}`
|
||||
: `/blogs/${encodeURIComponent(contentId)}`;
|
||||
case 'market': return `/market/${encodeURIComponent(contentId)}`;
|
||||
case 'report': return `/reports/${encodeURIComponent(contentId)}`;
|
||||
case 'project': return `/projects/${encodeURIComponent(contentId)}`;
|
||||
|
|
@ -124,8 +152,11 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
switch (content.type) {
|
||||
case 'post':
|
||||
return div({ class: 'search-post' },
|
||||
content.contentWarning ? h2({ class: 'card-field' }, span({ class: 'card-value' }, content.contentWarning)) : null,
|
||||
content.text ? div({ class: 'card-field' }, span({ class: 'card-value', [RAW_HTML]: sanitizeHtml(renderStyledHtml(content.text)) })) : null
|
||||
content.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, content.contentWarning)) : null,
|
||||
content.text ? div({ class: 'blog-card-text post-text-clamped', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(content.text).trim().length > 1000 ? String(content.text).trim().slice(0, 1000).replace(/\s+$/, '') + '…' : String(content.text).trim())) }) : null,
|
||||
Array.isArray(content.tags) && content.tags.length
|
||||
? div({ class: 'card-tags' }, content.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
|
||||
: null
|
||||
);
|
||||
case 'about':
|
||||
return div({ class: 'card-section about' },
|
||||
|
|
@ -596,6 +627,12 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
}
|
||||
};
|
||||
|
||||
const typeLabel = (type) => String(i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || type || '').toUpperCase();
|
||||
const totalResults = Object.values(results).reduce((n, msgs) => n + (Array.isArray(msgs) ? msgs.length : 0), 0);
|
||||
const resultStats = renderModuleStats(totalResults,
|
||||
Object.entries(results)
|
||||
.map(([type, msgs]) => ({ label: typeLabel(type), count: Array.isArray(msgs) ? msgs.length : 0 }))
|
||||
.sort((x, y) => y.count - x.count || x.label.localeCompare(y.label)));
|
||||
const pageGroups = paged(Object.entries(results).flatMap(([key, msgs]) => msgs.map((msg) => [key, msg])))
|
||||
.reduce((acc, [key, msg]) => { (acc[key] = acc[key] || []).push(msg); return acc; }, {});
|
||||
const resultSection = Object.entries(results).length > 0
|
||||
|
|
@ -642,7 +679,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
div({ class: 'card-header activity-card-header' },
|
||||
div({ class: 'card-chips-row' },
|
||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||
span({ class: 'pm-exposition-text' }, String(content.type || '').toUpperCase())
|
||||
span({ class: 'pm-exposition-text' }, String(i18n['type' + String(content.type || '').charAt(0).toUpperCase() + String(content.type || '').slice(1)] || content.type || '').toUpperCase())
|
||||
)
|
||||
),
|
||||
renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content), {
|
||||
|
|
@ -676,46 +713,18 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types =
|
|||
: null);
|
||||
|
||||
let html = template(
|
||||
hashtag ? `#${hashtag}` : i18n.searchTitle,
|
||||
i18n.searchTitle,
|
||||
section(
|
||||
div({ class: "tags-header module-header-line" },
|
||||
h2(hashtag ? `#${hashtag}` : i18n.searchTitle),
|
||||
h2(i18n.searchTitle),
|
||||
p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)
|
||||
),
|
||||
form(
|
||||
{ action: "/search", method: "GET", class: "search-form" },
|
||||
div({ class: "search-filters-row" },
|
||||
table({ class: "search-filters-table" },
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_from" }, i18n.searchFromLabel || "From")),
|
||||
td(input({ id: "search_from", type: "datetime-local", name: "from" }))
|
||||
),
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_to" }, i18n.searchToLabel || "To")),
|
||||
td(input({ id: "search_to", type: "datetime-local", name: "to" }))
|
||||
),
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_inhabitant" }, "Oasis ID")),
|
||||
td(input({ id: "search_inhabitant", type: "text", name: "inhabitant", placeholder: "@...=.ed25519", pattern: "@[A-Za-z0-9+/_\\-]{43}=\\.ed25519", maxlength: 56, class: "search-oasis-id" }))
|
||||
)
|
||||
),
|
||||
table({ class: "search-filters-table" },
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "search_query" }, i18n.searchQueryLabel || "Query")),
|
||||
td(searchInput)
|
||||
),
|
||||
tr(
|
||||
td({ class: 'card-label' }, label({ for: "results-per-page" }, i18n.searchPerPageLabel || "Results per page")),
|
||||
td(resultsPerPageSelect)
|
||||
)
|
||||
)
|
||||
),
|
||||
div({ class: "search-submit-row" },
|
||||
button({ type: "submit" }, i18n.searchSubmit)
|
||||
)
|
||||
)
|
||||
renderSearchForm({ action: "/search", query })
|
||||
),
|
||||
section(resultSection)
|
||||
section(
|
||||
resultStats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, resultStats) : null,
|
||||
resultSection
|
||||
)
|
||||
);
|
||||
|
||||
if (hasDocument) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ const { form, button, div, h2, h3, p, section, select, option, input, br, a, lab
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getConfig } = require('../configs/config-manager.js');
|
||||
const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES } = require('./main_views');
|
||||
const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, userLink } = require('./main_views');
|
||||
const i18nBase = require("../client/assets/translations/i18n");
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model');
|
||||
const html = require("../server/node_modules/hyperaxe");
|
||||
// selector de identidades: la implementacion vive en ./identities_view.js
|
||||
|
|
@ -12,6 +13,8 @@ const { renderVerificationReport, renderRebuildReport } = require('./backup_view
|
|||
|
||||
const snhUrl = "https://wiki.solarnethub.com/socialnet/overview";
|
||||
const BLOB_CACHE_OPTIONS = [512, 1024, 2048, 5120, 10240, 0];
|
||||
const plainLabel = (s) => String(s || '').replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim();
|
||||
const hiddenAt = (at) => at ? moment(at).format("YYYY/MM/DD HH:mm") : "—";
|
||||
const sizeLabel = (bytes) => {
|
||||
let value = Number(bytes) || 0;
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
|
|
@ -44,7 +47,9 @@ const settingsView = (params) => {
|
|||
const walletUser = currentConfig.wallet.user;
|
||||
const walletFee = currentConfig.wallet.fee;
|
||||
const currentWish = ['mutuals', 'only-lan', 'local'].includes(currentConfig.wish) ? currentConfig.wish : 'whole';
|
||||
const currentPmVisibility = currentConfig.pmVisibility === 'mutuals' ? 'mutuals' : 'whole';
|
||||
const currentPmVisibility = ['mutuals', 'following'].includes(currentConfig.pmVisibility) ? currentConfig.pmVisibility : 'whole';
|
||||
const currentMentionsVisibility = ['mutuals', 'following'].includes(currentConfig.mentionsVisibility) ? currentConfig.mentionsVisibility : 'whole';
|
||||
const viewerHidden = (() => { try { const vf = require('../models/viewer_filters'); return { muted: vf.mutedList(), hidden: vf.hiddenList() }; } catch (_) { return { muted: [], hidden: [] }; } })();
|
||||
const currentPhone = currentConfig.phone && typeof currentConfig.phone === 'object' ? currentConfig.phone : {};
|
||||
|
||||
const themeElements = THEMES.map(([value, name]) => option({ value, ...(theme === value ? { selected: true } : {}) }, name));
|
||||
|
|
@ -115,7 +120,7 @@ const settingsView = (params) => {
|
|||
);
|
||||
}
|
||||
|
||||
const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : 'oasis';
|
||||
const filter = params.filter === 'multiverse' && modOn('fediverse') ? 'multiverse' : params.filter === 'content' ? 'content' : 'oasis';
|
||||
const oasisParts = [
|
||||
updatePanel,
|
||||
renderIdentities(require('../server/ssb_config').account),
|
||||
|
|
@ -156,7 +161,8 @@ const settingsView = (params) => {
|
|||
const aiNavEnabled = currentConfig.modules && currentConfig.modules.aiNavMod === 'on';
|
||||
const chatsEnabled = currentConfig.modules && currentConfig.modules.chatsMod === 'on';
|
||||
const phoneEnabled = !!(currentConfig.modules && currentConfig.modules.phoneMod !== 'off');
|
||||
const cur = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : "blocks";
|
||||
const roomsEnabled = !!(currentConfig.modules && currentConfig.modules.roomsMod !== 'off');
|
||||
const cur = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : currentConfig.ux?.current === "feed" ? "feed" : currentConfig.ux?.current === "phone" ? "phone" : currentConfig.ux?.current === "rooms" ? "rooms" : "blocks";
|
||||
const uxCard = (value, title, image) => label({ class: "welcome-ux-option" },
|
||||
input({ type: "radio", name: "ux", value, ...(cur === value ? { checked: true } : {}) }),
|
||||
img({ src: image, class: "welcome-ux-shot", alt: title }),
|
||||
|
|
@ -167,7 +173,8 @@ const settingsView = (params) => {
|
|||
aiNavEnabled ? uxCard("ainav", i18n.uxModeAINav || "AI", "/assets/images/ux-ainav.png") : null,
|
||||
chatsEnabled ? uxCard("chats", i18n.uxModeChats || "Conversations", "/assets/images/ux-chats.png") : null,
|
||||
uxCard("feed", i18n.uxModeFeed || "Microblogging", "/assets/images/ux-feed.png"),
|
||||
phoneEnabled ? uxCard("phone", i18n.phoneTitle || "Phone", "/assets/images/ux-phone.png") : null
|
||||
phoneEnabled ? uxCard("phone", i18n.phoneTitle || "Phone", "/assets/images/ux-phone.png") : null,
|
||||
roomsEnabled ? uxCard("rooms", i18n.roomsTitle || "Rooms", "/assets/images/ux-rooms.png") : null
|
||||
);
|
||||
})(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
|
|
@ -187,22 +194,6 @@ const settingsView = (params) => {
|
|||
)
|
||||
)
|
||||
),
|
||||
section({ id: "wish" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsWishTitle),
|
||||
p(i18n.settingsWishDesc),
|
||||
form(
|
||||
{ action: "/settings/wish", method: "POST" },
|
||||
select({ name: "wish" },
|
||||
option({ value: "whole", ...(currentWish === "whole" ? { selected: true } : {})}, i18n.settingsWishWhole),
|
||||
option({ value: "mutuals", ...(currentWish === "mutuals" ? { selected: true } : {})}, i18n.settingsWishMutuals),
|
||||
option({ value: "only-lan", ...(currentWish === "only-lan" ? { selected: true } : {})}, i18n.settingsWishOnlyLan || "Only LAN"),
|
||||
option({ value: "local", ...(currentWish === "local" ? { selected: true } : {})}, i18n.settingsWishLocal || "Local")
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
workflowSection,
|
||||
section({ id: "home-page" },
|
||||
div({ class: "tags-header" },
|
||||
|
|
@ -282,48 +273,10 @@ const settingsView = (params) => {
|
|||
)
|
||||
)
|
||||
),
|
||||
blobCache ? section({ id: "blobcache" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsBlobCacheTitle),
|
||||
p(i18n.settingsBlobCacheDesc),
|
||||
blobCache.usage ? p({ class: "blob-cache-usage" }, strong(`${i18n.settingsBlobCacheUsage}: `), `${sizeLabel(blobCache.usage.bytes)} · ${blobCache.usage.count} ${i18n.settingsBlobCacheFiles}`) : null,
|
||||
blobCache.cleaned !== null ? p({ class: "blob-cache-notice" }, String(i18n.settingsBlobCacheCleaned).replace('{n}', String(blobCache.cleaned)).replace('{size}', sizeLabel(blobCache.freed))) : null,
|
||||
form(
|
||||
{ action: "/settings/blob-cache", method: "POST" },
|
||||
label({ for: "blob_cache_mb" }, i18n.settingsBlobCacheLimit),
|
||||
br(),
|
||||
select({ id: "blob_cache_mb", name: "blob_cache_mb" }, ...BLOB_CACHE_OPTIONS.map(mb => option({ value: String(mb), ...(mb === blobCache.maxMB ? { selected: true } : {}) }, blobCacheOptionLabel(mb)))),
|
||||
br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings),
|
||||
blobCache.maxMB > 0 ? button({ type: "submit", formaction: "/settings/blob-cache/collect", class: "blob-cache-clean-btn" }, i18n.settingsBlobCacheCleanNow) : null
|
||||
)
|
||||
)
|
||||
) : null,
|
||||
section({ id: "replication" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsReplicationTitle || 'Replication'),
|
||||
p(i18n.settingsReplicationDesc || 'Configure the number of hops your peer follows out from your own feed when replicating content.'),
|
||||
form(
|
||||
{ action: "/settings/replication", method: "POST" },
|
||||
label({ for: "replication_hops" }, i18n.settingsReplicationHopsLabel || 'Hops'),
|
||||
br(),
|
||||
input({
|
||||
type: "number",
|
||||
id: "replication_hops",
|
||||
name: "hops",
|
||||
min: 0,
|
||||
max: 6,
|
||||
value: currentHops
|
||||
}),
|
||||
br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
section({ id: "lan" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsLanTitle || 'LAN Broadcasting'),
|
||||
p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network. Disable to stop UDP broadcasts.'),
|
||||
p(i18n.settingsLanDesc || 'Periodically announce this peer to other Oasis instances on the same local network.'),
|
||||
form(
|
||||
{ action: "/settings/lan-broadcasting", method: "POST" },
|
||||
label({ for: "lanBroadcasting", class: "lan-checkbox-label" },
|
||||
|
|
@ -342,20 +295,6 @@ const settingsView = (params) => {
|
|||
)
|
||||
)
|
||||
),
|
||||
section({ id: "pm-visibility" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsPmVisibilityTitle),
|
||||
p(i18n.settingsPmVisibilityDesc),
|
||||
form(
|
||||
{ action: "/settings/pm-visibility", method: "POST" },
|
||||
select({ name: "pmVisibility" },
|
||||
option({ value: "whole", ...(currentPmVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole),
|
||||
option({ value: "mutuals", ...(currentPmVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals)
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
modOn('phone') ? section({ id: "phone" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.phoneTitle),
|
||||
|
|
@ -376,30 +315,6 @@ const settingsView = (params) => {
|
|||
)
|
||||
)
|
||||
) : null,
|
||||
modOn('inbox') ? section({ id: "inbox-bots" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsInboxBotsTitle),
|
||||
p(i18n.settingsInboxBotsDesc),
|
||||
form(
|
||||
{ action: "/settings/inbox-bots", method: "POST" },
|
||||
...INBOX_BOT_ORDER.map(bot =>
|
||||
label({ for: `inbox-bot-${bot}`, class: "lan-checkbox-label inbox-bot-label" },
|
||||
input({
|
||||
type: "checkbox",
|
||||
id: `inbox-bot-${bot}`,
|
||||
name: "bots",
|
||||
value: bot,
|
||||
class: "lan-checkbox-input",
|
||||
checked: (Array.isArray(currentConfig.inboxMutedBots) && currentConfig.inboxMutedBots.includes(bot)) ? undefined : true
|
||||
}),
|
||||
span({ class: "lan-checkbox-text" }, inboxBotLabel(bot))
|
||||
)
|
||||
),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
) : null,
|
||||
modOn('wallet') ? section(
|
||||
{ id: "wallet" },
|
||||
div({ class: "tags-header" },
|
||||
|
|
@ -461,6 +376,156 @@ const settingsView = (params) => {
|
|||
)
|
||||
)
|
||||
];
|
||||
const contentParts = [
|
||||
section({ id: "wish" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsWishTitle),
|
||||
p(i18n.settingsWishDesc),
|
||||
form(
|
||||
{ action: "/settings/wish", method: "POST" },
|
||||
select({ name: "wish" },
|
||||
option({ value: "whole", ...(currentWish === "whole" ? { selected: true } : {})}, i18n.settingsWishWhole),
|
||||
option({ value: "mutuals", ...(currentWish === "mutuals" ? { selected: true } : {})}, i18n.settingsWishMutuals),
|
||||
option({ value: "only-lan", ...(currentWish === "only-lan" ? { selected: true } : {})}, i18n.settingsWishOnlyLan || "Only LAN"),
|
||||
option({ value: "local", ...(currentWish === "local" ? { selected: true } : {})}, i18n.settingsWishLocal || "Local")
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
section({ id: "replication" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsReplicationTitle || 'Replication'),
|
||||
p(i18n.settingsReplicationDesc || 'Configure the number of hops your peer follows out from your own feed when replicating content.'),
|
||||
form(
|
||||
{ action: "/settings/replication", method: "POST" },
|
||||
label({ for: "replication_hops" }, i18n.settingsReplicationHopsLabel || 'Hops'),
|
||||
br(),
|
||||
input({
|
||||
type: "number",
|
||||
id: "replication_hops",
|
||||
name: "hops",
|
||||
min: 0,
|
||||
max: 6,
|
||||
value: currentHops
|
||||
}),
|
||||
br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
blobCache ? section({ id: "blobcache" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsBlobCacheTitle),
|
||||
p(i18n.settingsBlobCacheDesc),
|
||||
blobCache.usage ? p({ class: "blob-cache-usage" }, strong(`${i18n.settingsBlobCacheUsage}: `), `${sizeLabel(blobCache.usage.bytes)} · ${blobCache.usage.count} ${i18n.settingsBlobCacheFiles}`) : null,
|
||||
blobCache.cleaned !== null ? p({ class: "blob-cache-notice" }, String(i18n.settingsBlobCacheCleaned).replace('{n}', String(blobCache.cleaned)).replace('{size}', sizeLabel(blobCache.freed))) : null,
|
||||
form(
|
||||
{ action: "/settings/blob-cache", method: "POST" },
|
||||
label({ for: "blob_cache_mb" }, i18n.settingsBlobCacheLimit),
|
||||
br(),
|
||||
select({ id: "blob_cache_mb", name: "blob_cache_mb" }, ...BLOB_CACHE_OPTIONS.map(mb => option({ value: String(mb), ...(mb === blobCache.maxMB ? { selected: true } : {}) }, blobCacheOptionLabel(mb)))),
|
||||
br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings),
|
||||
blobCache.maxMB > 0 ? button({ type: "submit", formaction: "/settings/blob-cache/collect", class: "blob-cache-clean-btn" }, i18n.settingsBlobCacheCleanNow) : null
|
||||
)
|
||||
)
|
||||
) : null,
|
||||
section({ id: "pm-visibility" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsPmVisibilityTitle),
|
||||
p(i18n.settingsPmVisibilityDesc),
|
||||
form(
|
||||
{ action: "/settings/pm-visibility", method: "POST" },
|
||||
select({ name: "pmVisibility" },
|
||||
option({ value: "whole", ...(currentPmVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole),
|
||||
option({ value: "following", ...(currentPmVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing),
|
||||
option({ value: "mutuals", ...(currentPmVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals)
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
section({ id: "mentions-visibility" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsMentionsVisibilityTitle),
|
||||
p(i18n.settingsMentionsVisibilityDesc),
|
||||
form(
|
||||
{ action: "/settings/mentions-visibility", method: "POST" },
|
||||
select({ name: "mentionsVisibility" },
|
||||
option({ value: "whole", ...(currentMentionsVisibility === "whole" ? { selected: true } : {})}, i18n.settingsPmVisibilityWhole),
|
||||
option({ value: "following", ...(currentMentionsVisibility === "following" ? { selected: true } : {})}, i18n.settingsPmVisibilityFollowing),
|
||||
option({ value: "mutuals", ...(currentMentionsVisibility === "mutuals" ? { selected: true } : {})}, i18n.settingsPmVisibilityMutuals)
|
||||
), br(), br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
),
|
||||
modOn('inbox') ? section({ id: "inbox-bots" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsInboxBotsTitle),
|
||||
p(i18n.settingsInboxBotsDesc),
|
||||
form(
|
||||
{ action: "/settings/inbox-bots", method: "POST" },
|
||||
...INBOX_BOT_ORDER.map(bot =>
|
||||
label({ for: `inbox-bot-${bot}`, class: "lan-checkbox-label inbox-bot-label" },
|
||||
input({
|
||||
type: "checkbox",
|
||||
id: `inbox-bot-${bot}`,
|
||||
name: "bots",
|
||||
value: bot,
|
||||
class: "lan-checkbox-input",
|
||||
checked: (Array.isArray(currentConfig.inboxMutedBots) && currentConfig.inboxMutedBots.includes(bot)) ? undefined : true
|
||||
}),
|
||||
span({ class: "lan-checkbox-text" }, inboxBotLabel(bot))
|
||||
)
|
||||
),
|
||||
br(),
|
||||
button({ type: "submit" }, i18n.saveSettings)
|
||||
)
|
||||
)
|
||||
) : null,
|
||||
section({ id: "hidden" },
|
||||
div({ class: "tags-header" },
|
||||
h2(i18n.settingsHiddenTitle),
|
||||
p(i18n.settingsHiddenDesc),
|
||||
p(strong(`${i18n.settingsMutedLabel} (${viewerHidden.muted.length})`)),
|
||||
viewerHidden.muted.length
|
||||
? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" },
|
||||
tr(
|
||||
td({ class: "card-label" }, i18n.inhabitantProfileTitle),
|
||||
td({ class: "card-label" }, i18n.date),
|
||||
td({ class: "card-label" }, "")
|
||||
),
|
||||
...viewerHidden.muted.map(m => tr(
|
||||
td({ "data-label": i18n.inhabitantProfileTitle }, userLink(m.id)),
|
||||
td({ "data-label": i18n.date }, hiddenAt(m.at)),
|
||||
td({ "data-label": "" }, form({ action: `/unmute/${encodeURIComponent(m.id)}`, method: "POST" },
|
||||
input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.unmute)))
|
||||
))
|
||||
))
|
||||
: p({ class: "muted" }, i18n.settingsMutedEmpty),
|
||||
p(strong(`${i18n.settingsHiddenLabel} (${viewerHidden.hidden.length})`)),
|
||||
viewerHidden.hidden.length
|
||||
? div({ class: "viewer-hidden-block" }, table({ class: "block-info-table" },
|
||||
tr(
|
||||
td({ class: "card-label" }, i18n.settingsTabContent),
|
||||
td({ class: "card-label" }, i18n.date),
|
||||
td({ class: "card-label" }, "")
|
||||
),
|
||||
...viewerHidden.hidden.map(h => tr(
|
||||
td({ "data-label": i18n.settingsTabContent }, h.href ? a({ href: h.href }, plainLabel(h.label) || h.id) : span(plainLabel(h.label) || h.id)),
|
||||
td({ "data-label": i18n.date }, hiddenAt(h.at)),
|
||||
td({ "data-label": "" }, form({ action: `/unhide/${encodeURIComponent(h.id)}`, method: "POST" },
|
||||
input({ type: "hidden", name: "returnTo", value: "/settings?filter=content#hidden" }),
|
||||
button({ type: "submit", class: "filter-btn" }, i18n.unhideContent)))
|
||||
))
|
||||
))
|
||||
: p({ class: "muted" }, i18n.settingsHiddenEmpty)
|
||||
)
|
||||
),
|
||||
];
|
||||
const multiverseParts = modOn('fediverse') ? [
|
||||
section({ id: "mastodon" },
|
||||
div({ class: "tags-header" },
|
||||
|
|
@ -586,10 +651,11 @@ const settingsView = (params) => {
|
|||
),
|
||||
div({ class: "mode-buttons-row" },
|
||||
a({ class: filter === 'oasis' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=oasis' }, String(i18n.linkBoxOasis).toUpperCase()),
|
||||
a({ class: filter === 'content' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=content' }, String(i18n.settingsTabContent).toUpperCase()),
|
||||
modOn('fediverse') ? a({ class: filter === 'multiverse' ? 'filter-btn active' : 'filter-btn', href: '/settings?filter=multiverse' }, String(i18n.multiverse).toUpperCase()) : null
|
||||
)
|
||||
),
|
||||
...(filter === 'multiverse' ? multiverseParts : oasisParts)
|
||||
...(filter === 'multiverse' ? multiverseParts : filter === 'content' ? contentParts : oasisParts)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -80,10 +80,11 @@ const renderTorrentSeeds = (t, spreadMap) => {
|
|||
const info = spreadInfoOf(t, spreadMap);
|
||||
return String(info && typeof info.count === "number" ? info.count : (info && Array.isArray(info.voters) ? info.voters.length : 0));
|
||||
};
|
||||
const renderTorrentSpread = (t, spreadMap) => String(t.author) === String(userId) ? "" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
||||
const renderTorrentSpread = (t, spreadMap) => String(t.author) === String(userId) ? "—" : renderSpreadButton(t.key, spreadInfoOf(t, spreadMap), i18n.seedAction, false);
|
||||
|
||||
const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, params = {}) => {
|
||||
const returnTo = buildReturnTo(filter, params);
|
||||
const extraColumns = typeof params.rowFor === "function";
|
||||
|
||||
if (!torrents.length) return p(params.q ? i18n.torrentNoMatch : i18n.noTorrents);
|
||||
|
||||
|
|
@ -91,21 +92,18 @@ const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, param
|
|||
{ border: "1", class: "torrent-table" },
|
||||
tr(
|
||||
th(i18n.createdAt || "DATE"),
|
||||
th(i18n.authorLabel || "AUTHOR"),
|
||||
th(i18n.torrentTitleLabel || "TITLE"),
|
||||
th(i18n.torrentSizeLabel || "SIZE"),
|
||||
th(i18n.seedsLabel),
|
||||
th(i18n.seedAction),
|
||||
th({ class: "torrent-spread-cell" }, i18n.seedsLabel),
|
||||
th({ class: "torrent-spread-cell" }, i18n.seedAction),
|
||||
th(""),
|
||||
th(""),
|
||||
th("")
|
||||
...(extraColumns ? [th(""), th("")] : [])
|
||||
),
|
||||
torrents.map((t) => {
|
||||
const row = typeof params.rowFor === "function" ? params.rowFor(t) : null;
|
||||
return tr(
|
||||
td({ class: "row-chips" }, t.tribeOrigin ? renderTribeOriginChip(t.tribeOrigin) : t.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('torrents', t.title, t.key)) : null, ...(row && Array.isArray(row.titleChips) ? row.titleChips.filter(Boolean) : [])),
|
||||
td({ "data-label": i18n.createdAt || "DATE" }, moment(t.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
td({ class: "row-author" }, userLink(t.author)),
|
||||
td({ class: "row-title", "data-label": i18n.torrentTitleLabel || "TITLE" }, t.title || ""),
|
||||
td({ "data-label": i18n.torrentSizeLabel || "SIZE" }, formatSize(t.size)),
|
||||
td({ class: "torrent-spread-cell", "data-label": i18n.seedsLabel }, t.tribeOrigin || row ? "" : renderTorrentSeeds(t, params.spreadMap)),
|
||||
|
|
@ -124,25 +122,7 @@ const renderTorrentTable = exports.renderTorrentTable = (torrents, filter, param
|
|||
button({ type: "submit", class: "filter-btn" }, i18n.torrentDetailsButton)
|
||||
)
|
||||
),
|
||||
td(
|
||||
row
|
||||
? (row.download || "")
|
||||
: t.url && t.url.startsWith("&")
|
||||
? div({ class: "torrent-card-actions" }, renderTorrentSourceDownload(t.key, t.source), renderTorrentDownload(torrentDownloadHref(t.url, t.title)))
|
||||
: ""
|
||||
),
|
||||
td({ class: "row-actions" },
|
||||
row
|
||||
? (row.actions || "")
|
||||
: t.tribeOrigin ? "" : renderContentActions(t.key, `/torrents/${encodeURIComponent(t.key)}`, {
|
||||
author: t.author,
|
||||
favKind: 'torrents',
|
||||
isFavorite: t.isFavorite,
|
||||
returnTo,
|
||||
reportTitle: t.title,
|
||||
deleteAction: String(t.author) === String(userId) ? contentDeleteAction('torrent', t.key) : undefined
|
||||
})
|
||||
)
|
||||
...(extraColumns ? [td(row ? (row.download || "") : ""), td({ class: "row-actions" }, row ? (row.actions || "") : "")] : [])
|
||||
);
|
||||
})
|
||||
);
|
||||
|
|
@ -308,7 +288,6 @@ exports.torrentsView = async (torrents, filter = "all", torrentId = null, params
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()),
|
||||
|
|
@ -449,7 +428,7 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
|||
: null
|
||||
);
|
||||
})(),
|
||||
renderEngagement(torrentObj.key,
|
||||
torrentObj.tribeId ? null : renderEngagement(torrentObj.key,
|
||||
renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants),
|
||||
renderTorrentCommentsSection(torrentObj.key, comments, returnTo)
|
||||
)
|
||||
|
|
@ -466,7 +445,6 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []),
|
||||
|
|
|
|||
|
|
@ -295,7 +295,6 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "minAmount", value: String(minAmountRaw ?? "") }),
|
||||
input({ type: "hidden", name: "maxAmount", value: String(maxAmountRaw ?? "") }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
|
|
@ -558,7 +557,6 @@ exports.singleTransferView = async (transfer, filter, params = {}) => {
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/transfers", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "minAmount", value: String(params.minAmount ?? "") }),
|
||||
input({ type: "hidden", name: "maxAmount", value: String(params.maxAmount ?? "") }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ const { sanitizeHtml } = require('../backend/sanitizeHtml');
|
|||
|
||||
const userId = config.keys.id;
|
||||
|
||||
const capFirst = (s) => String(s || '').charAt(0).toUpperCase() + String(s || '').slice(1);
|
||||
const typeLabel = (type) => i18n['type' + capFirst(type)] || type;
|
||||
|
||||
const filterButton = (mode, currentFilter) =>
|
||||
form({ method: 'GET', action: '/trending' },
|
||||
input({ type: 'hidden', name: 'filter', value: mode }),
|
||||
button(
|
||||
{ type: 'submit', class: currentFilter === mode ? 'filter-btn active' : 'filter-btn' },
|
||||
String(i18n[mode + 'Button'] || mode).toUpperCase()
|
||||
String(i18n[mode + 'Button'] || typeLabel(mode)).toUpperCase()
|
||||
)
|
||||
);
|
||||
|
||||
|
|
@ -162,6 +165,16 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
: null
|
||||
)
|
||||
);
|
||||
} else if (c.type === 'post') {
|
||||
contentHtml = div({ class: 'trending-blog' },
|
||||
div({ class: 'card-section blog' },
|
||||
c.contentWarning ? div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href: `/blogs/${encodeURIComponent(item.key)}` }, c.contentWarning))) : "",
|
||||
div({ class: 'blog-card-text', [RAW_HTML]: sanitizeHtml(renderStyledHtml(String(c.text || '').trim().length > 420 ? String(c.text).trim().slice(0, 420).replace(/\s+$/, '') + '…' : String(c.text || '').trim())) }),
|
||||
Array.isArray(c.tags) && c.tags.length
|
||||
? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
|
||||
: null
|
||||
)
|
||||
);
|
||||
} else if (c.type === 'feed') {
|
||||
const { text, refeeds } = c;
|
||||
contentHtml = div({ class: 'trending-feed' },
|
||||
|
|
@ -221,6 +234,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
file: 'files',
|
||||
document: 'documents',
|
||||
feed: 'feed',
|
||||
post: 'blogs',
|
||||
votes: 'votes',
|
||||
transfer: 'transfers',
|
||||
industry: 'industry',
|
||||
|
|
@ -249,12 +263,12 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
|
|||
div(
|
||||
{ class: 'card-header activity-card-header' },
|
||||
span({ class: 'pm-exposition-chip pm-exposition-whole' },
|
||||
span({ class: 'pm-exposition-text' }, String(i18n['type' + String(c.type || '').charAt(0).toUpperCase() + String(c.type || '').slice(1)] || c.type || '').toUpperCase())
|
||||
span({ class: 'pm-exposition-text' }, String(typeLabel(c.type) || '').toUpperCase())
|
||||
),
|
||||
renderContentActions(item.key, detailHref, {
|
||||
spread: spreadMap.get(item.key) || null,
|
||||
author: item.value.author,
|
||||
reportTitle: c.title || c.question || c.concept || '',
|
||||
reportTitle: c.title || c.question || c.concept || c.contentWarning || '',
|
||||
...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&')
|
||||
? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } }
|
||||
: {}),
|
||||
|
|
@ -301,7 +315,7 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap
|
|||
const baseFilters = ['ALL', 'RECENT', 'MINE', 'TOP'];
|
||||
const contentFilters = [
|
||||
['votes', 'event', 'task', 'report'],
|
||||
['feed', 'project', 'industry', 'shopProduct', 'transfer'],
|
||||
['feed', 'post', 'project', 'industry', 'shopProduct', 'transfer'],
|
||||
['audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video'],
|
||||
['podcast', 'podcastEpisode', 'campaign', 'logisticsRoute']
|
||||
];
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const { renderCalendarCard } = require('./calendars_view');
|
|||
const { renderInhabitantCard } = require('./inhabitants_view');
|
||||
const { config } = require('../server/SSB_server.js');
|
||||
const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText');
|
||||
const { renderSearchForm } = require('./search_view');
|
||||
const { renderMapCard, renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
||||
const { renderTribeRoomsSection, roomStatsBy } = require('./rooms_view');
|
||||
|
||||
|
|
@ -110,18 +111,19 @@ const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => {
|
|||
if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||
if (s.startsWith('/assets/') || s.startsWith('/blob/') || s.startsWith('/image/')) return img({ loading: 'lazy', src: s, ...attrs })
|
||||
if (s.startsWith('&')) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(s)}`, ...attrs })
|
||||
const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
||||
if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' })
|
||||
const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
||||
if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` })
|
||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
|
||||
if (mImg) return img({ loading: 'lazy', src: `/blob/${encodeURIComponent(mImg[1])}`, ...attrs })
|
||||
const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||
if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: toBlobUrl(mVideo[1]), preload: 'metadata' })
|
||||
const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||
if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: toBlobUrl(mAudio[1]) })
|
||||
const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256|\/blob\/sealed\/[^)\s]+)\s*\)/)
|
||||
if (mImg) return img({ loading: 'lazy', src: toBlobUrl(mImg[1]), ...attrs })
|
||||
return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null
|
||||
}
|
||||
|
||||
const toBlobUrl = (raw) => {
|
||||
if (!raw) return null
|
||||
const s = String(raw).trim()
|
||||
if (s.startsWith('/blob/sealed/')) return s
|
||||
if (s.startsWith('&')) return `/blob/${encodeURIComponent(s)}`
|
||||
const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/)
|
||||
return m ? `/blob/${encodeURIComponent(m[1])}` : null
|
||||
|
|
@ -387,6 +389,15 @@ const sectionLink = (tribe, sectionKey, label, currentSection) =>
|
|||
button({ type: 'submit', class: currentSection === sectionKey ? 'filter-btn active' : 'filter-btn' }, String(label).toUpperCase())
|
||||
);
|
||||
|
||||
const tribeTagHref = (tribe) => (tag) => `/tribe/${encodeURIComponent(tribe.id)}?section=search&q=${encodeURIComponent('#' + String(tag).toLowerCase().replace(/^#+/, ''))}`;
|
||||
const tribeText = (tribe, text, opts = {}) => renderStyledText(String(text || ''), { ...opts, ...(tribe && tribe.id ? { hashtagHref: tribeTagHref(tribe) } : {}) });
|
||||
const tribeExcerpt = (tribe, text, max = 200) => {
|
||||
const full = String(text || '').trim();
|
||||
if (!full) return null;
|
||||
const cut = full.length > max ? full.slice(0, max).replace(/\s+$/, '') + '…' : full;
|
||||
return p({ class: 'post-text post-text-pre' }, ...tribeText(tribe, cut));
|
||||
};
|
||||
|
||||
const renderSectionNav = (tribe, section) => {
|
||||
const firstGroup = [{ key: 'activity', label: i18n.tribeSectionActivity }, { key: 'inhabitants', label: i18n.tribeSectionInhabitants }];
|
||||
if (!tribe.parentTribeId) firstGroup.push({ key: 'subtribes', label: i18n.tribeSubTribes });
|
||||
|
|
@ -449,7 +460,7 @@ const contentTypeVerb = (ct) => {
|
|||
};
|
||||
|
||||
const contentTypeName = (ct) => {
|
||||
const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, poll: i18n.pollsTitle, forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, blog: i18n.blogTitle, media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS', wiki: i18n.tribeSectionWiki || 'WIKI' };
|
||||
const map = { event: i18n.tribeSectionEvents, task: i18n.tribeSectionTasks, votation: i18n.tribeSectionVotations, poll: String(i18n.pollsTitle || 'POLLS').toUpperCase(), forum: i18n.tribeSectionForum, 'forum-reply': i18n.tribeSectionForum, blog: String(i18n.blogTitle || 'BLOGS').toUpperCase(), media: i18n.tribeSectionMedia, feed: i18n.tribeSectionFeed, pad: i18n.tribeSectionPads || 'PADS', chat: i18n.tribeSectionChats || 'CHATS', calendar: i18n.tribeSectionCalendars || 'CALENDARS', map: i18n.tribeSectionMaps || 'MAPS', wiki: i18n.tribeSectionWiki || 'WIKI' };
|
||||
return map[ct] || ct;
|
||||
};
|
||||
|
||||
|
|
@ -494,7 +505,7 @@ const renderTribeActivitySection = (tribe, sectionData) => {
|
|||
),
|
||||
div({ class: 'tribe-card-body' },
|
||||
item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null,
|
||||
item.description ? p(String(item.description).substring(0, 200)) : null,
|
||||
tribeExcerpt(tribe, item.description),
|
||||
replies.map(r => div({ class: 'thread-reply-item' },
|
||||
r.text ? p({ class: 'post-text post-text-pre' }, String(r.text)) : null,
|
||||
div({ class: 'card-footer thread-reply-footer' },
|
||||
|
|
@ -546,7 +557,7 @@ const renderTribeActivitySection = (tribe, sectionData) => {
|
|||
span({ class: 'card-value' }, item.title)
|
||||
) : null,
|
||||
mediaContent,
|
||||
item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? p(item.description.substring(0, 200)) : null
|
||||
item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? tribeExcerpt(tribe, item.description) : null
|
||||
),
|
||||
p({ class: 'card-footer' },
|
||||
span({ class: 'date-link' }, `${date}`),
|
||||
|
|
@ -589,11 +600,8 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => {
|
|||
|
||||
const renderTribeTagsSection = (tribe, sectionData, query) => {
|
||||
const tagsList = Array.isArray(sectionData?.tags) ? sectionData.tags : [];
|
||||
const selectedTag = sectionData?.selectedTag || '';
|
||||
const filteredItems = Array.isArray(sectionData?.filteredItems) ? sectionData.filteredItems : [];
|
||||
const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`;
|
||||
const sortedTags = tagsList.slice().sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
||||
const threadsById = reachIndexOf(tagsList.flatMap(t => Array.isArray(t.items) ? t.items : []));
|
||||
const tagHref = tribeTagHref(tribe);
|
||||
return section(
|
||||
div({ class: 'tags-header module-header-line' },
|
||||
h2(i18n.tribeSectionTags || i18n.tagsTitle || 'TAGS'),
|
||||
|
|
@ -608,29 +616,13 @@ const renderTribeTagsSection = (tribe, sectionData, query) => {
|
|||
th(i18n.tagsTableHeaderCount || 'Count')
|
||||
)),
|
||||
tbody(
|
||||
(selectedTag ? sortedTags : paged(sortedTags)).map(t => tr(
|
||||
td(a({ href: `${tribeUrl}?section=tags&tag=${encodeURIComponent(t.tag)}` }, t.tag)),
|
||||
paged(sortedTags).map(t => tr(
|
||||
td(a({ href: tagHref(t.tag) }, t.tag)),
|
||||
td(`${t.count}`)
|
||||
))
|
||||
)
|
||||
)
|
||||
),
|
||||
selectedTag ? div({ class: 'tribe-content-list' },
|
||||
h2(`#${selectedTag} (${filteredItems.length})`),
|
||||
filteredItems.length === 0 ? p(i18n.tribeTagsEmpty || 'No items') :
|
||||
paged(filteredItems).map(item => div({ class: 'card card-rpg' },
|
||||
div({ class: 'card-header' }, h2({ class: 'card-label' }, `[${String(contentTypeName(item.contentType)).toUpperCase()}]`)),
|
||||
div({ class: 'card-body' },
|
||||
renderTribeReachListChip(tribe, item, threadsById),
|
||||
item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null,
|
||||
item.description ? p(item.description.substring(0, 200)) : null
|
||||
),
|
||||
p({ class: 'card-footer' },
|
||||
span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
userLink(item.author)
|
||||
)
|
||||
))
|
||||
) : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -639,16 +631,19 @@ const renderTribeSearchSection = (tribe, sectionData, query) => {
|
|||
const sq = sectionData?.query || '';
|
||||
const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`;
|
||||
const threadsById = reachIndexOf(results);
|
||||
const hashtag = sq.trim().startsWith('#') ? sq.trim().replace(/^#+/, '').toLowerCase() : '';
|
||||
const typeCounts = new Map();
|
||||
for (const item of results) {
|
||||
const label = String(contentTypeName(item.contentType)).toUpperCase();
|
||||
typeCounts.set(label, (typeCounts.get(label) || 0) + 1);
|
||||
}
|
||||
const stats = renderModuleStats(results.length, [...typeCounts].map(([label, count]) => ({ label, count })).sort((x, y) => y.count - x.count || x.label.localeCompare(y.label)));
|
||||
return div({ class: 'tribe-content-list' },
|
||||
div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionSearch)),
|
||||
form({ method: 'GET', action: tribeUrl, class: 'tribe-search-form' },
|
||||
input({ type: 'hidden', name: 'section', value: 'search' }),
|
||||
input({ type: 'text', name: 'q', value: sq, placeholder: i18n.tribeSearchPlaceholder, minlength: 2 }),
|
||||
button({ type: 'submit', class: 'create-button' }, i18n.tribeSectionSearch)
|
||||
),
|
||||
div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionSearch), p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)),
|
||||
renderSearchForm({ action: tribeUrl, query: sq, queryName: 'q', hidden: [['section', 'search']] }),
|
||||
sq.length > 0 && sq.length < 2 ? p(i18n.tribeSearchMinChars) : null,
|
||||
sq.length >= 2 ? div(
|
||||
h2(`${i18n.tribeSearchResults}: ${results.length}`),
|
||||
stats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, stats) : null,
|
||||
results.length === 0 ? p(i18n.tribeSearchEmpty) :
|
||||
paged(results).map(item => div({ class: 'card card-rpg' },
|
||||
div({ class: 'card-header' },
|
||||
|
|
@ -660,7 +655,7 @@ const renderTribeSearchSection = (tribe, sectionData, query) => {
|
|||
span({ class: 'card-label' }, (i18n.title || 'Title') + ':'),
|
||||
span({ class: 'card-value' }, item.title)
|
||||
) : null,
|
||||
item.description ? p(item.description.substring(0, 200)) : null
|
||||
tribeExcerpt(tribe, item.description)
|
||||
),
|
||||
p({ class: 'card-footer' },
|
||||
span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")),
|
||||
|
|
@ -686,7 +681,7 @@ const renderOverviewSection = (tribe, query, sectionData) => {
|
|||
recentFeed.map(m =>
|
||||
div({ class: 'feed-item' },
|
||||
p(`${moment(m.createdAt).format("YYYY/MM/DD HH:mm")} — `, userLink(m.author)),
|
||||
p(...renderStyledText(m.description))
|
||||
p(...tribeText(tribe, m.description))
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
@ -743,7 +738,7 @@ const tribeStatusOf = (item) => String(item.status || 'OPEN').toUpperCase();
|
|||
const capFirst = (s) => String(s).charAt(0).toUpperCase() + String(s).slice(1);
|
||||
const blobIdOf = (raw) => {
|
||||
const s = String(raw || '').trim();
|
||||
if (s.startsWith('&')) return s;
|
||||
if (s.startsWith('&') || s.startsWith('/blob/sealed/')) return s;
|
||||
const m = s.match(/\((&[^)\s]+\.sha256)\s*\)/);
|
||||
return m ? m[1] : null;
|
||||
};
|
||||
|
|
@ -864,6 +859,7 @@ const renderFeedTribeView = (feedItems, tribe, query = {}) => {
|
|||
const chip = tribeReachChipFor(tribe, m);
|
||||
const controls = renderTribeReachControls(tribe, m, returnTo);
|
||||
return renderFeedCard(toFeedShape(m), null, {
|
||||
hashtagHref: tribeTagHref(tribe),
|
||||
refeedAction: `${tribeUrl}/refeed/${encodeURIComponent(m.id)}`,
|
||||
canRefeed: member,
|
||||
headerActions: tribeItemActions(tribe, m, member, m.description),
|
||||
|
|
@ -1193,7 +1189,7 @@ const renderForumThreadDetail = (tribe, allItems, threadId, member) => {
|
|||
reachChip
|
||||
),
|
||||
renderTribeReachControls(tribe, thread, returnTo),
|
||||
div({ class: 'forum-body' }, ...renderStyledText(thread.description || '')),
|
||||
div({ class: 'forum-body' }, ...tribeText(tribe, thread.description || '')),
|
||||
div({ class: 'forum-meta' },
|
||||
span({ class: 'forum-positive-votes' }, `▲: ${thread.refeeds || 0}`),
|
||||
span({ class: 'forum-participants' }, `${String(i18n.forumParticipants).toUpperCase()}: ${participants.size}`),
|
||||
|
|
@ -1227,7 +1223,7 @@ const renderForumThreadDetail = (tribe, allItems, threadId, member) => {
|
|||
div({ class: 'comment-body-row' },
|
||||
div({ class: 'comment-vote-col' }, forumScoreBox(tribe, r, member, threadId)),
|
||||
div({ class: 'comment-text-col' },
|
||||
div(...String(r.description || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => p(...renderStyledText(l, { zoomImages: true }))))
|
||||
div(...String(r.description || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => p(...tribeText(tribe, l, { zoomImages: true }))))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -1871,7 +1867,7 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f
|
|||
? `<div class="cn-filter-row">${btn(hrefFor('', activeAuthor), i18n.all, byAuthor.length, !activeType)}${[...typeCounts.entries()].map(([k, n]) => btn(hrefFor(k, activeAuthor), kindName(byAuthor.find(it => kindKey(it) === k)), n, activeType === k)).join('')}</div>`
|
||||
: '';
|
||||
const authorRow = authorCounts.size > 1
|
||||
? `<div class="cn-filter-row">${btn(hrefFor(activeType, ''), i18n.all, [...authorCounts.values()].reduce((a, b) => a + b, 0), !activeAuthor)}${[...authorCounts.entries()].map(([id, n]) => btn(hrefFor(activeType, id), who(id), n, activeAuthor === id)).join('')}</div>`
|
||||
? `<div class="cn-filter-row">${[...authorCounts.entries()].map(([id, n]) => btn(hrefFor(activeType, activeAuthor === id ? '' : id), who(id), n, activeAuthor === id)).join('')}</div>`
|
||||
: '';
|
||||
const renderItem = (it) => {
|
||||
const mt = it.contentType === 'media' ? String(it.mediaType || '') : '';
|
||||
|
|
@ -1879,9 +1875,9 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f
|
|||
const title = escapeHtml(it.title || '');
|
||||
const blob = cnBlobUrl(it.image);
|
||||
const media = !blob ? ''
|
||||
: mt === 'image' ? `<img class="cn-tribe-img" src="${blob}" alt="${title}" loading="lazy"/>`
|
||||
: mt === 'audio' ? `<audio class="cn-tribe-player" controls preload="metadata" src="${blob}"></audio>`
|
||||
: mt === 'video' ? `<video class="cn-tribe-player" controls preload="metadata" src="${blob}"></video>`
|
||||
: mt === 'image' ? `<img class="post-image" src="${blob}" alt="${title}" loading="lazy"/>`
|
||||
: mt === 'audio' ? `<audio class="post-audio" controls preload="metadata" src="${blob}"></audio>`
|
||||
: mt === 'video' ? `<video class="post-video" controls preload="metadata" src="${blob}"></video>`
|
||||
: ['document', 'file', 'torrent'].includes(mt) ? `<a class="cn-tribe-link" href="${blob}" rel="noopener">⇩ ${title || escapeHtml(kind)}</a>`
|
||||
: '';
|
||||
const url = mt === 'bookmark' ? String(it.url || '') : '';
|
||||
|
|
@ -1929,8 +1925,6 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f
|
|||
.cn-tribe-meta{display:flex;gap:12px;flex-wrap:wrap;align-items:baseline;color:var(--fg-dim);font-size:12px;word-break:break-all}
|
||||
.cn-tribe-title{color:var(--fg);font-size:18px;font-weight:700;margin:8px 0;word-break:break-word}
|
||||
.cn-tribe-text{color:var(--fg-soft);line-height:1.5;margin:8px 0;word-break:break-word}
|
||||
.cn-tribe-img{display:block;max-width:100%;height:auto;border-radius:6px;border:1px solid var(--border);margin:10px 0}
|
||||
.cn-tribe-player{display:block;width:100%;max-width:100%;margin:10px 0;border-radius:6px;background:#000}
|
||||
.cn-tribe-link{display:inline-block;margin:8px 0;padding:6px 12px;background:var(--bg-sub);border:1px solid var(--border);border-radius:6px;color:var(--fg);word-break:break-all}
|
||||
.cn-tribe-options{margin:8px 0;padding-left:20px;color:var(--fg-soft)}
|
||||
.cn-tribe-replies{margin-top:12px;border-top:1px solid var(--border);padding-top:8px}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink }
|
|||
|
||||
const moment = require("../server/node_modules/moment");
|
||||
const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view');
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions , renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views");
|
||||
const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views");
|
||||
const { config } = require("../server/SSB_server.js");
|
||||
const { renderStyledText } = require("../backend/renderStyledText")
|
||||
const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view");
|
||||
|
|
@ -55,7 +55,7 @@ const renderVideoPlayer = (videoObj) =>
|
|||
{ class: "video-container video-container-row" },
|
||||
videoHyperaxe({
|
||||
controls: true,
|
||||
src: `/blob/${encodeURIComponent(videoObj.url)}`,
|
||||
src: blobUrlOf(videoObj.url),
|
||||
preload: "metadata"
|
||||
})
|
||||
)
|
||||
|
|
@ -232,7 +232,6 @@ exports.videoView = async (videos, filter = "all", videoId = null, params = {})
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
...(emptyMod ? [] : [
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()),
|
||||
|
|
@ -370,7 +369,6 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params
|
|||
{ class: "filters" },
|
||||
form(
|
||||
{ method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" },
|
||||
input({ type: "hidden", name: "q", value: q }),
|
||||
input({ type: "hidden", name: "sort", value: sort }),
|
||||
button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button(
|
||||
...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []),
|
||||
|
|
|
|||
|
|
@ -37,14 +37,15 @@ const stepAction = (key, profile, status) => {
|
|||
if (key === "ux") {
|
||||
let cfg = {}
|
||||
try { cfg = require("../configs/config-manager.js").getConfig() || {} } catch (_) {}
|
||||
const cur = ["ainav", "chats", "feed", "phone"].includes(cfg.ux?.current) ? cfg.ux.current : "blocks"
|
||||
const cur = ["ainav", "chats", "feed", "phone", "rooms"].includes(cfg.ux?.current) ? cfg.ux.current : "blocks"
|
||||
return form({ method: "POST", action: "/welcome/ux", class: "welcome-ux-form" },
|
||||
div({ class: "welcome-ux-grid" },
|
||||
pickCard("ux", "blocks", cur, i18n.uxModeMenus, "/assets/images/ux-blocks.png"),
|
||||
cfg.modules?.aiNavMod === "on" ? pickCard("ux", "ainav", cur, i18n.uxModeAINav, "/assets/images/ux-ainav.png") : null,
|
||||
cfg.modules?.chatsMod === "on" ? pickCard("ux", "chats", cur, i18n.uxModeChats, "/assets/images/ux-chats.png") : null,
|
||||
pickCard("ux", "feed", cur, i18n.uxModeFeed, "/assets/images/ux-feed.png"),
|
||||
cfg.modules?.phoneMod !== "off" ? pickCard("ux", "phone", cur, i18n.phoneTitle, "/assets/images/ux-phone.png") : null
|
||||
cfg.modules?.phoneMod !== "off" ? pickCard("ux", "phone", cur, i18n.phoneTitle, "/assets/images/ux-phone.png") : null,
|
||||
cfg.modules?.roomsMod !== "off" ? pickCard("ux", "rooms", cur, i18n.roomsTitle, "/assets/images/ux-rooms.png") : null
|
||||
),
|
||||
submitRow(i18n.welcomeStepUxAction)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ const renderFilters = (filter, q, params = {}, census = []) => {
|
|||
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" },
|
||||
input({ type: "hidden", name: "q", value: q || "" }),
|
||||
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))),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue