From 373fba5d969b6e577d4dc824998f6a7434c95ee1 Mon Sep 17 00:00:00 2001 From: SITO Date: Tue, 18 Aug 2026 22:45:35 +0200 Subject: [PATCH] views: main_views a 0.9.5 conservando la interfaz de la rama main_views.js pasa a la version de 0.9.5 y se le reinyectan los seis puntos propios: el enganche a fork/hive_nav, la topbar y los hexagonos en la cabecera, la barra inferior antes del pie, el orden invertido de hojas de estilo en movil (en los dos templates), el campo para copiar el Oasis ID y el chip de dispositivo. Esto era necesario, no cosmetico: el renderContentActions de la rama tenia dos parametros y el de 0.9.5 tiene tres. Las vistas ya integradas lo llaman con el tercero (favoritos, difusion, reportar) y la rama lo ignoraba en silencio, de modo que esos botones no llegaban a dibujarse. Cuidado con el orden de hojas: 0.9.5 carga el tema antes de mobile.css, lo que en movil dejaba a mobile.css ganando por cascada. Corregido en los dos templates. bookmark_view pasa a 0.9.5 con el renderPMButton de la rama reinyectado. El renderFavoriteToggle propio desaparece porque 0.9.5 integra los favoritos dentro de renderContentActions, que cubre lo mismo. Verificado con la app en marcha: 15 rutas OK, orden de hojas correcto en el HTML servido (style.css, mobile.css, OasisMobile.css), 10 categorias y 47 modulos en los hexagonos, topbar con Personal/Community y avatar, 7 accesos rapidos, campo de Oasis ID presente y 0 etiquetas vacias. --- src/views/bookmark_view.js | 229 ++------ src/views/main_views.js | 1058 ++++++++++++++++++------------------ 2 files changed, 582 insertions(+), 705 deletions(-) diff --git a/src/views/bookmark_view.js b/src/views/bookmark_view.js index 24381ba..671b4cf 100644 --- a/src/views/bookmark_view.js +++ b/src/views/bookmark_view.js @@ -1,7 +1,8 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option } = require("../server/node_modules/hyperaxe"); +const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderUrl } = require("../backend/renderUrl"); @@ -58,70 +59,11 @@ const renderBookmarkActions = (filter, bookmark, params = {}) => { }; const renderBookmarkCommentsSection = (bookmarkId, rootId, comments = [], returnTo = null) => { - const list = safeArr(comments).filter(c => { - const t = c && c.value && c.value.content && c.value.content.text; - return t && String(t).trim(); + return renderSharedCommentsSection({ + action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, + comments: comments, + returnTo: returnTo }); - const commentsCount = list.length; - - return div( - { class: "vote-comments-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ": "), - span({ class: "card-value" }, String(commentsCount)) - ), - div( - { class: "comment-form-wrapper" }, - h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), - form( - { method: "POST", action: `/bookmarks/${encodeURIComponent(bookmarkId)}/comments`, class: "comment-form", enctype: "multipart/form-data" }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - rootId ? input({ type: "hidden", name: "rootId", value: rootId }) : null, - textarea({ - id: "comment-text", - name: "text", - rows: 4, - class: "comment-textarea", - placeholder: i18n.voteNewCommentPlaceholder - }), - div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), - br(), - button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) - ) - ), - list.length - ? div( - { class: "comments-list" }, - list.map((c) => { - const author = c?.value?.author || ""; - const ts = c?.value?.timestamp || c?.timestamp; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm:ss") : ""; - const relDate = ts ? moment(ts).fromNow() : ""; - - const content = c?.value?.content || {}; - const text = content.text || ""; - const threadRoot = content.fork || content.root || null; - - return div( - { class: "votations-comment-card" }, - span( - { class: "created-at" }, - span(i18n.createdBy), - author ? userLink(author) : span("(unknown)"), - absDate ? span(" | ") : "", - absDate ? span({ class: "votations-comment-date" }, absDate) : "", - relDate ? span({ class: "votations-comment-date" }, " | ", i18n.sendTime) : "", - relDate && threadRoot - ? a({ href: `/thread/${encodeURIComponent(threadRoot)}#${encodeURIComponent(c.key)}` }, relDate) - : "" - ), - p({ class: "votations-comment-text" }, ...renderUrl(text)) - ); - }) - ) - : p({ class: "votations-no-comments" }, i18n.voteNoCommentsYet) - ); }; const renderCardField = (labelText, value) => @@ -131,22 +73,6 @@ const renderCardField = (labelText, value) => span({ class: "card-value" }, value) ); -const renderFavoriteToggle = (bookmark, returnTo) => - form( - { - method: "POST", - action: bookmark.isFavorite - ? `/bookmarks/favorites/remove/${encodeURIComponent(bookmark.id)}` - : `/bookmarks/favorites/add/${encodeURIComponent(bookmark.id)}`, - class: "bookmark-favorite-form" - }, - returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - button( - { type: "submit", class: "filter-btn" }, - bookmark.isFavorite ? i18n.bookmarkRemoveFavoriteButton : i18n.bookmarkAddFavoriteButton - ) - ); - const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); return list.length @@ -167,7 +93,7 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url @@ -180,52 +106,19 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { div( { class: "card-header activity-card-header" }, span(), - renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`) + renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { spread: (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads || null, author: bookmark.author, favKind: 'bookmarks', isFavorite: bookmark.isFavorite, reportTitle: bookmark.title }) ), div( { class: "card-section bookmark-card-body" }, - div( - { class: "bookmark-topbar" }, - div( - { class: "bookmark-topbar-left" }, - renderPMButton(bookmark.author), - renderFavoriteToggle(bookmark, returnTo) - ), - div( - { class: "bookmark-topbar-right" }, - renderBookmarkActions(filter, bookmark, params) - ) - ), - h2({ class: "bookmark-title" }, bookmark.category || bookmark.url || ""), + h2({ class: "bookmark-title" }, bookmark.url ? urlLink : (bookmark.title || "")), bookmark.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(bookmark.lifetime, i18n)) : null, - renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), + bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null, renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), br, - div( - { class: "card-comments-summary" }, - span({ class: "card-label" }, i18n.voteCommentsLabel + ":"), - span({ class: "card-value" }, String(commentCount)), - br(), - br(), - form( - { method: "GET", action: `/bookmarks/${encodeURIComponent(bookmark.id)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - input({ type: "hidden", name: "filter", value: filter || "all" }), - params.q ? input({ type: "hidden", name: "q", value: params.q }) : null, - params.sort ? input({ type: "hidden", name: "sort", value: params.sort }) : null, - button({ type: "submit", class: "filter-btn" }, i18n.voteCommentsForumButton) - ) + renderEngagement(bookmark.id, + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), + renderCommentsLink({ href: `/bookmarks/${encodeURIComponent(bookmark.id)}`, count: commentCount }) ), - div( - { class: "card-comments-summary feed-opinions-section" }, - div( - { class: "comments-count" }, - span({ class: "card-label" }, (i18n.opinionsTitle || "Opinions") + ": "), - span({ class: "card-value" }, String(Object.values(bookmark.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0))) - ), - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants) - ), - div({ class: "card-spread-left" }, renderSpreadButton(bookmark.id, (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads)), (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; const updatedTs = bookmark.updatedAt ? new Date(bookmark.updatedAt).getTime() : NaN; @@ -233,12 +126,12 @@ const renderBookmarkList = (filteredBookmarks, filter, params = {}) => { return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); @@ -262,6 +155,7 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { return div( { class: "div-center bookmark-form" }, + params.spreadWarning || null, form( { action: filter === "edit" ? `/bookmarks/update/${encodeURIComponent(bookmarkId)}` : "/bookmarks/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: returnTo }), @@ -294,16 +188,6 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }), br(), br(), - label(i18n.bookmarkCategoryLabel), - br(), - input({ - type: "text", - name: "category", - id: "category", - placeholder: i18n.bookmarkCategoryPlaceholder, - value: filter === "edit" ? bookmarkToEdit.category || "" : "" - }), - br(), label(i18n.bookmarkTagsLabel), br(), input({ @@ -321,20 +205,8 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { }; exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, params = {}) => { - const title = - filter === "mine" - ? i18n.bookmarkMineSectionTitle - : filter === "create" - ? i18n.bookmarkCreateSectionTitle - : filter === "edit" - ? i18n.bookmarkUpdateSectionTitle - : filter === "recent" - ? i18n.bookmarkRecentSectionTitle - : filter === "top" - ? i18n.bookmarkTopSectionTitle - : filter === "favorites" - ? i18n.bookmarkFavoritesSectionTitle - : i18n.bookmarkAllSectionTitle; + const bookmarkEditWarning = filter === "edit" ? await renderSpreadEditWarning(bookmarkId) : null; + const title = i18n.bookmarkTitle; const q = safeText(params.q || ""); const sort = safeText(params.sort || "recent"); @@ -359,18 +231,18 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para { 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" }, i18n.bookmarkFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase()), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase()), + button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase()), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ) ), section( filter === "edit" || filter === "create" - ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter }) + ? renderBookmarkForm(filter, bookmarkId, bookmarkToEdit || {}, tags, { ...params, filter, spreadWarning: bookmarkEditWarning }) : section( div( { class: "bookmarks-search" }, @@ -382,9 +254,9 @@ exports.bookmarkView = async (bookmarks, filter = "all", bookmarkId = null, para { class: "filter-box__controls" }, select( { name: "sort", class: "filter-box__select" }, - option({ value: "recent", selected: sort === "recent" }, i18n.bookmarkSortRecent), - option({ value: "oldest", selected: sort === "oldest" }, i18n.bookmarkSortOldest), - option({ value: "top", selected: sort === "top" }, i18n.bookmarkSortTop) + option({ value: "recent", ...(sort === "recent" ? { selected: true } : {})}, i18n.bookmarkSortRecent), + option({ value: "oldest", ...(sort === "oldest" ? { selected: true } : {})}, i18n.bookmarkSortOldest), + option({ value: "top", ...(sort === "top" ? { selected: true } : {})}, i18n.bookmarkSortTop) ), button({ type: "submit", class: "filter-box__button" }, i18n.bookmarkSearchButton) ) @@ -409,7 +281,7 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; const lastVisitTxt = lastVisit && lastVisit.isValid() - ? `${lastVisit.format("YYYY/MM/DD HH:mm:ss")} (${lastVisit.fromNow()})` + ? `${lastVisit.format("YYYY/MM/DD HH:mm")} (${lastVisit.fromNow()})` : i18n.noLastVisit; const urlLink = bookmark.url @@ -422,10 +294,7 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par ].filter(Boolean); const sideActions = []; - sideActions.push(renderFavoriteToggle(bookmark, returnTo)); - if (bookmark.author && String(bookmark.author) !== String(userId)) { - sideActions.push(renderPMButton(bookmark.author)); - } + sideActions.push(renderPMButton(bookmark.author)); if (isAuthor && !hasOpinions) { sideActions.push(form( { method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` }, @@ -443,25 +312,33 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const tagsNode = renderTags(bookmark.tags); + const detailActions = div({ class: "card-header activity-card-header" }, + renderContentActions(bookmark.id, null, { + author: bookmark.author, + favKind: 'bookmarks', + isFavorite: bookmark.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: bookmark.title + }) + ); + const bookmarkSide = div({ class: "tribe-side" }, div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, bookmark.category || bookmark.url || ""), + h2({ class: "tribe-card-title" }, bookmark.url ? urlLink : (bookmark.title || "")), renderReachChip(isClearnet, i18n) ), + bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null, chips.length ? div({ class: "card-chips-row" }, ...chips) : null, safeText(bookmark.description) ? p({ class: "tribe-side-description" }, ...renderUrl(bookmark.description)) : null, - safeText(bookmark.category) - ? renderCardField(i18n.bookmarkCategoryLabel + ":", safeText(bookmark.category)) - : null, tagsNode, - div({ class: "card-spread-centered" }, renderSpreadButton(bookmark.id, params.spreads)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null ); const bookmarkMain = div({ class: "tribe-main" }, - renderCardField(i18n.bookmarkUrlLabel + ":", urlLink), + detailActions, renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), (() => { const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; @@ -469,18 +346,20 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); return p( { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm:ss")} ${i18n.performed} `), + span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")} ${i18n.performed} `), userLink(bookmark.author), showUpdated ? span( { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm:ss")}` + ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` ) : null ); })(), - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), - renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) + renderEngagement(bookmark.id, + renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), + renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) + ) ); return template( @@ -496,11 +375,11 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par { 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" }, i18n.bookmarkFilterAll), - button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterMine), - button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterTop), - button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterFavorites), - button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, i18n.bookmarkFilterRecent), + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterAll).toUpperCase()), + button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterMine).toUpperCase()), + button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterRecent).toUpperCase()), + button({ type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterFavorites).toUpperCase()), + button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.bookmarkFilterTop).toUpperCase()), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.bookmarkCreateButton) ) ), diff --git a/src/views/main_views.js b/src/views/main_views.js index c625479..1c6e01f 100644 --- a/src/views/main_views.js +++ b/src/views/main_views.js @@ -99,19 +99,97 @@ const renderLifespanChip = (lifetime, i18nObj) => { ); }; -const renderContentActions = (msgId, viewHref) => { +const DATE_TIME_FORMAT = "YYYY/MM/DD HH:mm"; +const DATE_FORMAT = "YYYY/MM/DD"; +const fmtDateTime = (v) => (v ? moment(v).format(DATE_TIME_FORMAT) : ""); +const fmtDay = (v) => (v ? moment(v).format(DATE_FORMAT) : ""); +exports.DATE_TIME_FORMAT = DATE_TIME_FORMAT; +exports.DATE_FORMAT = DATE_FORMAT; +exports.fmtDateTime = fmtDateTime; +exports.fmtDay = fmtDay; + +const renderContentActions = (msgId, viewHref, opts = {}) => { + const o = (opts && typeof opts === 'object') ? opts : {}; const blockId = (typeof msgId === 'string' && msgId.startsWith('%')) ? msgId : null; + const myId = (config.keys && config.keys.id) ? config.keys.id : ''; + + 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 ? '✜' : '✛') + ) + : null; + + const spreadBtn = o.spread !== undefined && blockId ? renderSpreadButton(blockId, o.spread) : null; + const chainBtn = blockId - ? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer || 'View blockexplorer' }, '⦿') + ? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, '⦿') : null; + const contentBtn = viewHref - ? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent || 'Visit content' }, '↗') + ? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, '↗') : null; - if (!chainBtn && !contentBtn) return null; - return div({ class: 'content-actions' }, chainBtn, contentBtn); + + 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 + ? a({ href: reportHref, class: 'btn-singleview btn-report', title: i18n.reportContent }, '⚑') + : 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 }, '✉') + : null; + + if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn) return null; + return div({ class: 'content-actions' }, spreadBtn, pinBtn, reportBtn, pmBtn, chainBtn, contentBtn); }; exports.renderContentActions = renderContentActions; +const renderDocumentActions = (kind, id, leadingNodes = []) => { + const base = id ? `/${kind}/${encodeURIComponent(id)}` : `/${kind}`; + return div({ class: 'doc-export-actions' }, + ...leadingNodes, + form({ method: 'GET', action: `${base}/pdf` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.generatePdf) + ), + form({ method: 'POST', action: `${base}/share` }, + button({ type: 'submit', class: 'filter-btn' }, i18n.sharePm) + ) + ); +}; +exports.renderDocumentActions = renderDocumentActions; + +const renderRelationshipBlock = (relationship, feedId) => { + const rel = relationship || {}; + if (rel.me) return span({ class: 'status you' }, i18n.relationshipYou); + const actions = []; + const addAction = (action) => actions.push( + form({ action: `/${action}/${encodeURIComponent(feedId)}`, method: 'post' }, + button({ type: 'submit', class: 'filter-btn' }, i18n[action]) + ) + ); + if (rel.following) addAction('unfollow'); + else if (rel.blocking) addAction('unblock'); + else { addAction('follow'); addAction('block'); } + return div({ class: 'relationship-status' }, + rel.blocking && rel.blockedBy + ? span({ class: 'status blocked' }, i18n.relationshipMutualBlock) + : [ + rel.blocking ? span({ class: 'status blocked' }, i18n.relationshipBlocking) : null, + rel.blockedBy ? span({ class: 'status blocked-by' }, i18n.relationshipBlockedBy) : null, + rel.following && rel.followsMe + ? span({ class: 'status mutual' }, i18n.relationshipMutuals) + : [ + span({ class: 'status supporting' }, rel.following ? i18n.relationshipFollowing : i18n.relationshipNone), + span({ class: 'status supported-by' }, rel.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) + ] + ], + actions.length ? div({ class: 'relationship-actions' }, ...actions) : null + ); +}; +exports.renderRelationshipBlock = renderRelationshipBlock; + exports.renderStateChip = renderStateChip; exports.renderOpenClosedChip = renderOpenClosedChip; exports.renderVisibilityChip = renderVisibilityChip; @@ -187,21 +265,11 @@ const renderSpreadButton = (msgKey, opts) => { const voters = Array.isArray(o.voters) ? o.voters : []; const count = typeof o.count === 'number' ? o.count : voters.length; const alreadySpread = o.alreadySpread === true; - const maxNames = 5; - const maxLen = 16; - const lastVoters = voters.slice(-maxNames); - const tooltipNames = lastVoters - .map(v => (v && typeof v === 'object' ? (v.name || v.key || '') : String(v || ''))) - .filter(Boolean) - .map(n => n.slice(0, maxLen)) - .join(', '); - const extra = count > maxNames ? ` +${count - maxNames} ${i18n.spreadMore || 'more'}` : ''; - const tooltip = count > 0 ? `${tooltipNames}${extra}` : (i18n.spreadHint || 'Spread this to your supporters (replicates via your feed).'); return form( { method: 'POST', action: `/spread/${encodeURIComponent(msgKey)}`, class: 'spread-form' }, button( - { type: 'submit', class: alreadySpread ? 'spread-btn spread-btn-on' : 'spread-btn', title: tooltip }, - `🔁 ${count}` + { type: 'submit', class: alreadySpread ? 'btn-singleview btn-spread-on' : 'btn-singleview', title: i18n.spreadContent }, + `⟳ ${count}` ) ); }; @@ -213,7 +281,16 @@ const aiNavResultsView = ({ query, results }) => { const safeResults = Array.isArray(results) ? results : []; const fmtScore = (s) => { const n = Number(s); - return Number.isFinite(n) ? n.toFixed(2) : '—'; + return Number.isFinite(n) ? `${Math.round(n * 100)}%` : '—'; + }; + const routeLabel = (p) => { + const [base, qs] = String(p || '').split('?'); + let label = decodeURIComponent(base).replace(/^\//, '').replace(/\//g, ' · '); + try { + const filter = new URLSearchParams(qs || '').get('filter'); + if (filter) label += ` · ${filter}`; + } catch (_) {} + return label.toUpperCase(); }; const splitTerms = (desc) => String(desc || '') .split(/[,;]/) @@ -231,7 +308,7 @@ const aiNavResultsView = ({ query, results }) => { safeResults.map(r => div({ class: 'ai-nav-result-card card-section' }, div({ class: 'card-field' }, span({ class: 'card-label' }, `${(i18n.aiNavResultMatch || 'Match').toUpperCase()}: ${fmtScore(r.score)}`), - span({ class: 'card-value' }, a({ href: r.path, class: 'filter-btn' }, r.path)) + span({ class: 'card-value' }, a({ href: r.path, class: 'filter-btn' }, routeLabel(r.path))) ) )) ) @@ -262,6 +339,29 @@ const renderEngagement = (id, opinionsNode, commentsNode) => { }; exports.renderEngagement = renderEngagement; +const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters) => { + const ops = opinions || {}; + const total = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); + const myId = (config.keys && config.keys.id) ? config.keys.id : ''; + const alreadyVoted = Array.isArray(voters) && myId ? voters.includes(myId) : false; + return details({ class: 'opinions-voting-collapse' }, + summary({ class: total > 0 ? 'opinions-summary engage-on' : 'opinions-summary' }, + span({ class: 'opinions-summary-icon' }, 'ꔍ'), + span({ class: 'opinions-summary-count' }, `(${total})`)), + div({ class: 'voting-buttons' }, + opinionCategoriesList.map((category) => + form({ method: 'POST', action: `${basePath}/${encodeURIComponent(id)}/${category}` }, + returnTo ? input({ type: 'hidden', name: 'returnTo', value: returnTo }) : null, + button({ class: alreadyVoted ? 'vote-btn disabled' : 'vote-btn', type: 'submit', ...(alreadyVoted ? { disabled: true } : {}) }, + `${String(i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category).toUpperCase()} [${ops[category] || 0}]`) + ) + ) + ), + alreadyVoted ? p({ class: 'muted' }, i18n.alreadyVoted) : null + ); +}; +exports.renderOpinionsVoting = renderOpinionsVoting; + let spreadsModel = null; const spreadsFor = async (msgId) => { if (!msgId || typeof msgId !== 'string' || !msgId.startsWith('%')) return 0; @@ -283,27 +383,6 @@ const renderSpreadEditWarning = async (msgId) => { }; exports.renderSpreadEditWarning = renderSpreadEditWarning; -const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters) => { - const ops = opinions || {}; - const total = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); - const myId = (config.keys && config.keys.id) ? config.keys.id : ''; - const alreadyVoted = Array.isArray(voters) && myId ? voters.includes(myId) : false; - return details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, - `${i18n.opinionsTitle || 'Opinions'} (${total})`), - div({ class: 'voting-buttons' }, - opinionCategoriesList.map((category) => - form({ method: 'POST', action: `${basePath}/${encodeURIComponent(id)}/${category}` }, - returnTo ? input({ type: 'hidden', name: 'returnTo', value: returnTo }) : null, - button({ class: alreadyVoted ? 'vote-btn disabled' : 'vote-btn', type: 'submit', ...(alreadyVoted ? { disabled: true } : {}) }, - `${i18n['vote' + category.charAt(0).toUpperCase() + category.slice(1)] || category} [${ops[category] || 0}]`) - ) - ) - ), - alreadyVoted ? p({ class: 'muted' }, i18n.alreadyVoted) : null - ); -}; -exports.renderOpinionsVoting = renderOpinionsVoting; // markdown const markdownUrl = "https://commonmark.org/help/"; @@ -321,6 +400,10 @@ const nbsp = "\xa0"; const { getConfig } = require('../configs/config-manager.js'); // menu INIT +const assetVersion = () => { + try { return encodeURIComponent(String(readPkg()?.version || '0')); } catch (_) { return '0'; } +}; + const readPkg = () => { const file = path.resolve(__dirname, "..", "server", "package.json"); try { @@ -376,9 +459,9 @@ const renderFooter = () => { br(), span({ class: "oasis-footer-carbon" }, span("HcT: "), - a({ href: "/stats?filter=ALL" }, hcT != null ? String(hcT) : '–'), + a({ href: "/stats?filter=ALL#carbon" }, hcT != null ? String(hcT) : '–'), span(" | HcH: "), - a({ href: "/stats?filter=MINE" }, hcH != null ? String(hcH) : '–') + a({ href: "/stats?filter=MINE#carbon" }, hcH != null ? String(hcH) : '–') ), br(), a( @@ -449,72 +532,35 @@ const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) => { ); }; -const renderPopularLink = () => { - const popularMod = getConfig().modules.popularMod === "on"; - return popularMod + + + + + +const renderPollsLink = () => { + const pollsMod = getConfig().modules.pollsMod === "on"; + return pollsMod ? navLink({ - href: "/public/popular/day", - emoji: "⌘", - text: i18n.popular, - class: "popular-link enabled" + href: "/polls", + emoji: "☑", + text: i18n.pollsTitle, + class: "polls-link enabled" }) : ""; }; -const renderTopicsLink = () => { - const topicsMod = getConfig().modules.topicsMod === "on"; - return topicsMod +const renderBlogsLink = () => { + const blogsMod = getConfig().modules.blogsMod === "on"; + return blogsMod ? navLink({ - href: "/public/latest/topics", - emoji: "ϟ", - text: i18n.topics, - class: "topics-link enabled" + href: "/blogs", + emoji: "✦", + text: i18n.blogTitle, + class: "blogs-link enabled" }) : ""; }; -const renderSummariesLink = () => { - const summariesMod = getConfig().modules.summariesMod === "on"; - if (summariesMod) { - return [ - navLink({ - href: "/public/latest/summaries", - emoji: "※", - text: i18n.summaries, - class: "summaries-link enabled" - }) - ]; - } - return ""; -}; - -const renderLatestLink = () => { - const latestMod = getConfig().modules.latestMod === "on"; - return latestMod - ? navLink({ - href: "/public/latest", - emoji: "☄", - text: i18n.latest, - class: "latest-link enabled" - }) - : ""; -}; - -const renderThreadsLink = () => { - const threadsMod = getConfig().modules.threadsMod === "on"; - if (threadsMod) { - return [ - navLink({ - href: "/public/latest/threads", - emoji: "♺", - text: i18n.threads, - class: "threads-link enabled" - }) - ]; - } - return ""; -}; - const renderInvitesLink = () => { const invitesMod = getConfig().modules.invitesMod === "on"; return invitesMod @@ -733,17 +779,6 @@ const renderTagsLink = () => { : ""; }; -const renderMultiverseLink = () => { - const multiverseMod = getConfig().modules.multiverseMod === "on"; - return multiverseMod - ? navLink({ - href: "/public/latest/extended", - emoji: "∞", - text: i18n.multiverse, - class: "multiverse-link enabled" - }) - : ""; -}; const renderMastodonLink = () => { const fediverseMod = getConfig().modules.fediverseMod === "on"; @@ -769,6 +804,19 @@ const renderMarketLink = () => { : ""; }; +const renderHousingLink = () => { + const housingMod = getConfig().modules.housingMod === "on"; + return housingMod + ? [ + navLink({ + href: "/housing", + emoji: "⌂", + text: i18n.housingTitle + }) + ] + : ""; +}; + const renderJobsLink = () => { const jobsMod = getConfig().modules.jobsMod === "on"; return jobsMod @@ -1129,7 +1177,6 @@ const renderTasksLink = () => { : ""; }; - // === Oasis UX: topbar + bottombar + hive === // La implementacion vive en ./fork/hive_nav.js para que las reescrituras de // upstream sobre este fichero no arrastren la interfaz de la rama. @@ -1143,21 +1190,21 @@ exports.renderHiveNav = renderHiveNav; const template = (titlePrefix, ...elements) => { const currentConfig = getConfig(); const theme = currentConfig.themes.current || "Dark-SNH"; - const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : "blocks"; - const hiveFilter = (typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || ""; + const uxMode = currentConfig.ux?.current === "ainav" ? "ainav" : currentConfig.ux?.current === "chats" ? "chats" : "blocks"; const themeLink = link({ rel: "stylesheet", - href: `/assets/themes/${theme}.css` + href: `/assets/themes/${theme}.css?v=${assetVersion()}` }); const nodes = html( { lang: "en" }, head( title(titlePrefix, " | Oasis"), - link({ rel: "stylesheet", href: "/assets/styles/style.css" }), - // móvil: el tema va DESPUÉS de mobile.css para ganar por cascada sin !important; escritorio: orden original de epsylon + link({ rel: "stylesheet", href: `/assets/styles/style.css?v=${assetVersion()}` }), + // movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important; + // escritorio: orden original de epsylon ...(process.env.OASIS_MOBILE === '1' - ? [ link({ rel: "stylesheet", href: "/assets/styles/mobile.css" }), themeLink ] - : [ themeLink, link({ rel: "stylesheet", href: "/assets/styles/mobile.css", media: "(max-width: 768px)" }) ]), + ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), themeLink ] + : [ themeLink, link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]), link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -1173,15 +1220,117 @@ const template = (titlePrefix, ...elements) => { div( { class: uxMode === "ainav" ? "header ainav-only" : "header" }, renderMobileTopbar(), - // hive (hexágonos) SOLO en la home; en el resto de páginas el top queda limpio (solo topbar) - ((typeof global !== "undefined" && global.__OASIS_SHOW_HIVE__) ? renderHiveNav() : "") + // hive (hexagonos) SOLO en la home; en el resto de paginas el top queda limpio + ((typeof global !== "undefined" && global.__OASIS_SHOW_HIVE__) ? renderHiveNav() : ""), + div( + { class: "top-bar-left" }, + a( + { class: "logo-icon", href: "/" }, + img({ + class: "logo-icon", + src: "/assets/images/snh-oasis.jpg", + alt: "Oasis Logo" + }) + ), + uxMode === "ainav" ? nav( + ul( + (() => { + const inboxCount = sharedState.getInboxCount(); + const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; + return li( + a({ href: "/inbox" }, + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + ) + ); + })(), + navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }), + navLink({ href: "/invites", emoji: "ꔮ", text: i18n.invites }) + ) + ) : nav( + ul( + (() => { + const mentionsCount = sharedState.getMentionsCount(); + const badge = mentionsCount > 0 ? span({ class: 'inbox-badge' }, String(mentionsCount)) : ''; + return li( + a({ href: "/mentions" }, + span({ class: "emoji" }, "✺"), nbsp, i18n.mentions, badge + ) + ); + })(), + (() => { + const inboxCount = sharedState.getInboxCount(); + const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : ''; + return li( + a({ href: "/inbox" }, + span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge + ) + ); + })(), + uxMode === "chats" + ? navLink({ href: "/settings", emoji: "⚙", text: i18n.settings }) + : navLink({ + href: "/pm", + emoji: "ꕕ", + text: i18n.privateMessage + }) + ) + ) + ), + (() => { + const aiNavOn = getConfig().modules.aiNavMod === 'on'; + if (!aiNavOn && uxMode !== 'ainav') return null; + return div( + { class: uxMode === 'ainav' ? "top-bar-center top-bar-center-ainav" : "top-bar-center" }, + form( + { method: 'POST', action: '/ai/ask', class: 'ai-ask-form' }, + input({ + type: 'text', + name: 'q', + class: 'ai-ask-input', + placeholder: i18n.aiNavPlaceholder || 'Where do you want to go?', + autocomplete: 'off', + maxlength: '300', + autofocus: uxMode === 'ainav' ? 'autofocus' : undefined + }), + button({ type: 'submit', class: 'ai-ask-btn' }, '➤') + ) + ); + })(), + uxMode === "ainav" ? div( + { class: "top-bar-right" }, + nav( + ul( + navLink({ href: "/activity", emoji: "ꔙ", text: i18n.activityTitle }), + navLink({ href: "/graphos", emoji: "ꕢ", text: i18n.graphos }), + navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }) + ) + ) + ) : uxMode === "chats" ? div( + { class: "top-bar-right" }, + nav( + ul( + navLink({ href: "/activity", emoji: "ꔙ", text: i18n.activityTitle }), + renderTrendingLink(), + navLink({ href: "/search", emoji: "ꔅ", text: i18n.searchTitle }) + ) + ) + ) : div( + { class: "top-bar-right" }, + nav( + ul( + navLink({ href: "/data", emoji: "⚯", text: i18n.dataTitle }), + renderTagsLink(), + navLink({ href: "/search", emoji: "ꔅ", text: i18n.searchTitle }) + ) + ) + ) ), (() => { const updateFlagPath = path.join(__dirname, '../server/.update_required'); if (fs.existsSync(updateFlagPath)) { return div( { class: "update-banner" }, - span({ class: "update-banner-icon" }, "⟳"), + span({ class: "update-banner-icon" }, "🛠️"), span({ class: "update-banner-text" }, i18n.updateBannerText), a({ href: "/settings", class: "update-banner-link" }, i18n.updateBannerAction) ); @@ -1206,9 +1355,192 @@ const template = (titlePrefix, ...elements) => { return null; } })(), + (() => { + try { + const { getConfig } = require('../configs/config-manager.js'); + if (getConfig().ai?.suggestions === false) return null; + } catch (_) {} + const suggestion = sharedState.getBestMatch ? sharedState.getBestMatch() : null; + if (!suggestion || !suggestion.href) return null; + if (sharedState.getDismissedSuggestion && sharedState.getDismissedSuggestion() === suggestion.href) return null; + return div( + { class: "update-banner ai-suggestion-banner" }, + span({ class: "update-banner-icon" }, "🤖"), + span({ class: "update-banner-text" }, i18n.aiSuggestionBanner), + a({ href: suggestion.href, class: "update-banner-link" }, `${suggestion.title} (${(((Number(suggestion.score) || 0) * 100).toFixed(1)).replace(/\.0$/, '')}%)`), + form( + { method: "POST", action: "/ai/suggestion/dismiss", class: "welcome-banner-close" }, + button({ type: "submit", class: "welcome-banner-close-btn" }, "✕") + ) + ); + })(), div( - { class: uxMode === "ainav" ? "main-content ainav-only" : "main-content" }, - main({ id: "content", class: "main-column" }, elements) + { class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : "main-content" }, + uxMode !== "blocks" ? null : div( + { class: "sidebar-left" }, + nav( + ul( + navGroup( + { + id: "personal", + emoji: "⚉", + title: i18n.menuPersonal + }, + navLink({ + href: "/profile", + emoji: "⚉", + text: i18n.profile + }), + navLink({ + href: "/cv", + emoji: "ꕛ", + text: i18n.cvTitle + }), + renderAgendaLink(), + renderFavoritesLink(), + renderLogsLink(), + renderInvitesLink(), + navLink({ + href: "/modules", + emoji: "ꗣ", + text: i18n.modules + }), + navLink({ + href: "/settings", + emoji: "⚙", + text: i18n.settings + }) + ), + navGroup( + { + id: "governance", + emoji: "⚖", + title: i18n.menuGovernance + }, + navLink({ + href: "/inhabitants", + emoji: "ꖘ", + text: i18n.inhabitantsLabel + }), + renderTribesLink(), + renderLarpLink(), + renderParliamentLink(), + renderCourtsLink() + ), + navGroup( + { + id: "economy", + emoji: "¤", + title: i18n.menuEconomy + }, + renderBankingLink(), + renderWalletLink(), + renderMarketLink(), + renderHousingLink(), + renderProjectsLink(), + renderIndustryLink(), + renderJobsLink(), + renderShopsLink(), + renderTransfersLink() + ), + navGroup( + { + id: "office", + emoji: "⌂", + title: i18n.menuOffice + }, + renderVotationsLink(), + renderPollsLink(), + renderEventsLink(), + renderCalendarsLink(), + renderTasksLink(), + renderReportsLink() + ), + navGroup( + { + id: "tools", + emoji: "⚒", + title: i18n.menuTools + }, + renderAILink(), + navLink({ + href: "/blockexplorer", + emoji: "ꖸ", + text: i18n.blockchain + }), + renderGraphosLink(), + navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }), + renderDevLink(), + renderCipherLink(), + renderLegacyLink(), + navLink({ + href: "/stats", + emoji: "ꕷ", + text: i18n.statistics + }) + ) + ) + ) + ), + main({ id: "content", class: "main-column" }, elements), + uxMode !== "blocks" ? null : div( + { class: "sidebar-right" }, + nav( + ul( + navGroup( + { + id: "network", + emoji: "☍", + title: i18n.menuNetwork + }, + navLink({ + href: "/activity", + emoji: "ꔙ", + text: i18n.activityTitle + }), + renderFeedLink(), + renderBlogsLink(), + renderTrendingLink(), + renderOpinionsLink(), + renderPadsLink(), + renderForumLink(), + renderMapsLink(), + renderChatsLink() + ), + navGroup( + { + id: "media", + emoji: "▤", + title: i18n.menuMedia + }, + renderAudiosLink(), + renderBookmarksLink(), + renderDocsLink(), + renderImagesLink(), + renderTorrentsLink(), + renderVideosLink() + ), + navGroup( + { + id: "creative", + emoji: "✎", + title: i18n.menuCreative + }, + renderGamesLink(), + renderPixeliaLink(), + renderMelodyLink() + ), + navGroup( + { + id: "fediverse", + emoji: "⌬", + title: i18n.fediverse + }, + renderMastodonLink() + ) + ) + ) + ) ), renderBottomBar(), renderFooter() @@ -1220,58 +1552,6 @@ const template = (titlePrefix, ...elements) => { exports.template = template; -exports.bottomBarPickerView = () => { - const cfg = getConfig(); - const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; - const MAX = 4; - const pinnedKeys = cfg.bottomBarPins || []; - const pinned = pinnedKeys - .map(key => exports.PINNABLE_MODULES.find(m => m.key === key)) - .filter(Boolean); - const full = pinned.length >= MAX; - const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls }, - input({ type: 'hidden', name: 'action', value: action }), - input({ type: 'hidden', name: 'key', value: key }), - node - ); - const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form', - button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' }, - span({ class: 'bb-manage-ico' }, m.glyph), - span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key), - span({ class: 'bb-manage-x' }, '✕') - ) - ); - const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form', - button({ type: 'submit', class: 'bb-pick-btn' }, - span({ class: 'bb-pick-ico' }, m.glyph), - span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key) - ) - ); - const addable = exports.PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key)); - return template( - i18n.bbManage || 'Edit bar', - section( - div({ class: 'tags-header' }, - h2(i18n.bbPickTitle || 'Customize the bar'), - p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.') - ), - div({ class: 'bb-manage' }, - h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`), - pinned.length - ? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow)) - : p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None') - ), - div({ class: 'bb-manage-add' }, - h3(i18n.bbAdd || 'Add'), - full - ? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached') - : div({ class: 'bb-pick-grid' }, ...addable.map(addBtn)) - ), - a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done') - ) - ); -}; - exports.ainavHomeView = ({ recentTags = [] } = {}) => { const currentConfig = getConfig(); const theme = currentConfig.themes.current || "Dark-SNH"; @@ -1280,11 +1560,12 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => { { lang: "en" }, head( title(placeholder, " | Oasis"), - link({ rel: "stylesheet", href: "/assets/styles/style.css" }), - // móvil: el tema va DESPUÉS de mobile.css para ganar por cascada sin !important; escritorio: orden original de epsylon + link({ rel: "stylesheet", href: `/assets/styles/style.css?v=${assetVersion()}` }), + // movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important; + // escritorio: orden original de epsylon ...(process.env.OASIS_MOBILE === '1' - ? [ link({ rel: "stylesheet", href: "/assets/styles/mobile.css" }), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css` }) ] - : [ link({ rel: "stylesheet", href: `/assets/themes/${theme}.css` }), link({ rel: "stylesheet", href: "/assets/styles/mobile.css", media: "(max-width: 768px)" }) ]), + ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }) ] + : [ link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]), link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -1827,10 +2108,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const fallbackVoted = !!msg.value?.meta?.voted; const fallbackVoteCount = msg.value?.meta?.votes?.length || 0; - const fallbackVoteNames = (msg.value?.meta?.votes || []) - .map((person) => person.name) - .filter(Boolean); - const spreadInfoObj = (spreadInfo && typeof spreadInfo === 'object') ? spreadInfo : null; const spreadCount = spreadInfoObj && typeof spreadInfoObj.count === 'number' ? spreadInfoObj.count @@ -1838,22 +2115,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const alreadySpread = spreadInfoObj && typeof spreadInfoObj.alreadySpread === 'boolean' ? spreadInfoObj.alreadySpread : fallbackVoted; - const spreadVoters = (spreadInfoObj && Array.isArray(spreadInfoObj.voters)) - ? spreadInfoObj.voters - .map(v => (v && typeof v === 'object') ? (v.name || v.key || '') : String(v || '')) - .filter(Boolean) - : fallbackVoteNames; - - const maxSpreadNameLength = 16; - const maxSpreadNames = 16; - const spreadByNames = spreadVoters - .slice(0, maxSpreadNames) - .map((n) => String(n).slice(0, maxSpreadNameLength)) - .join(", "); - const additionalSpreadsMessage = - spreadCount > maxSpreadNames ? `+${spreadCount - maxSpreadNames} more` : ``; - const spreadByMessage = - spreadCount > 0 ? `${spreadByNames} ${additionalSpreadsMessage}`.trim() : (i18n.spreadHint || 'Spread this to your supporters (replicates via your feed).'); const spreadButtonClass = alreadySpread ? 'liked' : null; const messageClasses = ["post"]; @@ -1928,7 +2189,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { { type: "submit", class: spreadButtonClass, - title: spreadByMessage, + title: i18n.spreadContent, }, `☉ ${spreadCount}` ) @@ -1983,7 +2244,7 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' ecoTax: visibilityPrefs.ecoTax !== false, larpSign: visibilityPrefs.larpSign === true, fediverse: visibilityPrefs.fediverse === true, - gpg: visibilityPrefs.gpg !== false + gpg: visibilityPrefs.gpg === true }; const fediverseHandleValue = typeof visibilityPrefs.fediverseHandle === 'string' ? visibilityPrefs.fediverseHandle : ''; prefs.clearnet = prefs.clearnetShops || prefs.clearnetJobs || prefs.clearnetEvents || prefs.clearnetProjects || prefs.clearnetPosts || prefs.clearnetAudios || prefs.clearnetVideos || prefs.clearnetImages || prefs.clearnetDocuments || prefs.clearnetTorrents || prefs.clearnetBookmarks; @@ -2033,7 +2294,7 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' div({ class: "gpg-edit-row" }, input({ type: "file", name: "gpgKey", accept: ".asc,.gpg,.pgp,application/pgp-keys,text/plain" }), gpgFingerprint - ? button({ type: "submit", formaction: "/profile/gpg/remove", formenctype: "application/x-www-form-urlencoded", formnovalidate: true, class: "gpg-remove-btn" }, + ? button({ type: "submit", formaction: "/profile/gpg/remove", attrs: { formenctype: "application/x-www-form-urlencoded", formnovalidate: "formnovalidate" }, class: "gpg-remove-btn" }, (i18n.profileGpgRemove || 'Remove') + ' (' + String(gpgFingerprint).slice(-8).toUpperCase() + ')' ) : null @@ -2068,7 +2329,7 @@ exports.editProfileView = ({ name, description, visibilityPrefs = {}, feedId = ' div({ class: "pref-pill-row" }, togglePill('karma', i18n.profileVisibilityKarma || 'KARMA Scoring'), togglePill('activity', i18n.profileVisibilityActivity || 'Activity Level'), - togglePill('fediverse', i18n.profileVisibilityFediverse || 'Fediverse'), + togglePill('fediverse', i18n.profileVisibilityFediverse || 'Multiverse'), togglePill('device', i18n.profileVisibilityDevice || 'Device', process.env.OASIS_MOBILE === '1'), togglePill('larpSign', i18n.profileVisibilityLarpSign || 'L.A.R.P. Sign'), togglePill('gpg', i18n.profileVisibilityGpg || 'GPG Key'), @@ -2367,7 +2628,7 @@ exports.authorView = async ({ const rawPrefs = visibilityPrefs || {}; const prefs = { activity: rawPrefs.activity === true, - device: rawPrefs.device !== false, + device: rawPrefs.device === true, karma: rawPrefs.karma !== false, ubi: rawPrefs.ubi === true, wallet: rawPrefs.wallet === true, @@ -2376,7 +2637,7 @@ exports.authorView = async ({ clearnet: rawPrefs.clearnet === true, fediverse: rawPrefs.fediverse === true, fediverseHandle: typeof rawPrefs.fediverseHandle === 'string' ? rawPrefs.fediverseHandle : '', - gpg: rawPrefs.gpg !== false + gpg: rawPrefs.gpg === true }; const clearnetSubKeys = ['clearnetShops','clearnetJobs','clearnetEvents','clearnetProjects','clearnetPosts','clearnetAudios','clearnetVideos','clearnetImages','clearnetDocuments','clearnetTorrents','clearnetBookmarks']; const anySubClearnet = clearnetSubKeys.some(k => rawPrefs[k] === true); @@ -2388,51 +2649,7 @@ exports.authorView = async ({ const escHtml = s => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); const markdownMention = `[@${escHtml(name)}](${escHtml(feedId)})`; - const contactForms = []; - const addForm = ({ action }) => - contactForms.push( - form( - { action: `/${action}/${encodeURIComponent(feedId)}`, method: "post" }, - button({ type: "submit", class: "btn" }, i18n[action]) - ) - ); - - if (relationship.me === false) { - if (relationship.following) addForm({ action: "unfollow" }); - else if (relationship.blocking) addForm({ action: "unblock" }); - else { addForm({ action: "follow" }); addForm({ action: "block" }) } - } - - const relationshipMessage = (() => { - if (relationship.me) return i18n.relationshipYou; - const following = relationship.following === true; - const followsMe = relationship.followsMe === true; - if (following && followsMe) return i18n.relationshipMutuals; - const messagesArr = []; - messagesArr.push(following ? i18n.relationshipFollowing : i18n.relationshipNone); - messagesArr.push(followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing); - return messagesArr.join(". ") + "."; - })(); - - const relationshipBlock = relationship.me - ? span({ class: "status you" }, i18n.relationshipYou) - : div({ class: "relationship-status" }, - relationship.blocking && relationship.blockedBy - ? span({ class: "status blocked" }, i18n.relationshipMutualBlock) - : [ - relationship.blocking ? span({ class: "status blocked" }, i18n.relationshipBlocking) : null, - relationship.blockedBy ? span({ class: "status blocked-by" }, i18n.relationshipBlockedBy) : null, - relationship.following && relationship.followsMe - ? span({ class: "status mutual" }, i18n.relationshipMutuals) - : [ - span({ class: "status supporting" }, relationship.following ? i18n.relationshipFollowing : i18n.relationshipNone), - span({ class: "status supported-by" }, relationship.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing) - ] - ], - contactForms.length - ? div({ class: "relationship-actions" }, ...contactForms) - : null - ); + const relationshipBlock = renderRelationshipBlock(relationship, feedId); const userSensors = renderUserSensors({ isMe: isOwnProfile, fediverseConfigured, prefs, id: feedId, @@ -2457,9 +2674,9 @@ exports.authorView = async ({ : null, ...userSensors, div({ class: "profile-side-actions" }, - isOwnProfile ? a({ href: `/profile/edit`, class: "btn" }, i18n.editProfile) : null, - a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes), - !isOwnProfile ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "btn" }, i18n.pmCreateButton) : null + isOwnProfile ? a({ href: `/profile/edit`, class: "filter-btn" }, i18n.editProfile) : null, + a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "filter-btn" }, i18n.viewLikes), + !isOwnProfile ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "filter-btn" }, i18n.pmCreateButton) : null ) ); @@ -2523,7 +2740,7 @@ exports.authorView = async ({ const { renderActionCards } = require('./activity_view'); mainColumnContent.push(filterRow); mainColumnContent.push(div({ class: 'feed-container profile-module-section' }, - renderActionCards(limited, feedId, allActions || limited, spreadMap instanceof Map ? spreadMap : new Map()) + renderActionCards(limited, (config.keys && config.keys.id) ? config.keys.id : '', allActions || limited, spreadMap instanceof Map ? spreadMap : new Map()) )); } } @@ -2724,77 +2941,7 @@ const renderMessage = (msg) => { ); }; -const hasMention = (msg, feedId) => { - const content = lodash.get(msg, "value.content", {}); - const mentions = content.mentions; - if (mentions) { - if (Array.isArray(mentions)) { - if (mentions.some(m => m.link === feedId || m.feed === feedId)) return true; - } else if (typeof mentions === 'object') { - for (const arr of Object.values(mentions)) { - if (Array.isArray(arr) && arr.some(m => m.link === feedId || m.feed === feedId)) return true; - if (arr && (arr.link === feedId || arr.feed === feedId)) return true; - } - } - } - const text = content.text || ''; - if (text.includes(feedId) || text.includes(feedId.slice(1))) return true; - return false; -}; -exports.mentionsView = ({ messages, myFeedId }) => { - const title = i18n.mentions; - const description = i18n.mentionsDescription; - if (!Array.isArray(messages) || messages.length === 0) { - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - p({ class: "empty" }, i18n.noMentions) - ) - ) - ); - } - const filteredMessages = messages - .filter(msg => hasMention(msg, myFeedId)) - .sort((a, b) => (b.value.timestamp || 0) - (a.value.timestamp || 0)); - if (filteredMessages.length === 0) { - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - p({ class: "empty" }, i18n.noMentions) - ) - ) - ); - } - return template( - title, - section( - div({ class: "tags-header" }, - h2(title), - p(description) - ) - ), - section( - div({ class: "mentions-list" }, - filteredMessages.map(renderMessage) - ) - ) - ); -}; exports.privateView = async (messagesInput, filter, decrypted = null, notice = '') => { const noticeText = notice === 'unavailable' @@ -2983,7 +3130,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' .replace(/\/ai\/ask\?[^\s<"]+/g, (match) => `${match}`) .replace(/\/tribe\/([%A-Za-z0-9/+._=-]+\.sha256)(\?section=[a-zA-Z]+)?/g, (match) => `${match}`) .replace(/\/larp\/([a-zA-Z]+)/g, (match) => `${match}`) - .replace(/(? `${match}`) + .replace(/(? `${match}`) .replace(/(https?:\/\/[^\s<"]+)/g, (match) => `${match}`) .replace(/\u0000MD(\d+)\u0000/g, (m, i) => mdLinks[Number(i)] !== undefined ? mdLinks[Number(i)] : m) } @@ -3157,6 +3304,41 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' ) } + function JobsBotCard({ sentAt, from, toLinks, text, key, msgSize }) { + const title = i18n.jobsBotMatchTitle + return div( + { class: 'pm-card jobs-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `💼 ${i18n.pmBotJobs} · ${title}`), + div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + + function HousingBotCard({ subjectU, sentAt, from, toLinks, text, key, msgSize }) { + const myId = (config.keys && config.keys.id) ? config.keys.id : '' + const mine = String(from) === String(myId) + const titleMap = mine + ? { + HOUSING_REQUESTED: i18n.housingBotYouRequestedTitle || 'You have requested a place.', + HOUSING_CANCELLED: i18n.housingBotYouCancelledTitle || 'You have cancelled a request.', + HOUSING_UNAVAILABLE: i18n.housingBotUnavailableSentTitle || 'You have told the people who requested this place.' + } + : { + HOUSING_REQUESTED: i18n.housingBotRequestedTitle || 'Somebody has requested one of your places.', + HOUSING_CANCELLED: i18n.housingBotCancelledTitle || 'A request on one of your places was cancelled.', + HOUSING_UNAVAILABLE: i18n.housingBotUnavailableTitle || 'A place you requested is no longer available.' + } + const title = titleMap[subjectU] || (i18n.pmBotHousing || 'HousingBot') + return div( + { class: 'pm-card housing-bot-notification thread-level-0' }, + headerLine({ sentAt, from, toLinks, subject: title, msgKey: key, msgSize }), + h2({ class: 'pm-title' }, `🏠 ${i18n.pmBotHousing || 'HousingBot'} · ${title}`), + div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text || '')) }), + actions({ key, replyId: from, subjectRaw: title, text }) + ) + } + const { renderEncryptedChip } = require('./clearnet_view'); return template( i18n.private, @@ -3242,7 +3424,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' span({ class: 'pm-fileshare-meta' }, `${fmtBytes(fsp.size)} · ${String(fsp.mime || 'application/octet-stream')}`) ), fsp.crypter - ? form({ method: 'GET', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-fileshare-download-form' }, + ? form({ method: 'POST', action: `/inbox/file/${encodeURIComponent(msg.key)}`, class: 'pm-fileshare-download-form' }, dblKey && dblKey.error ? div({ class: 'pm-form-error-msg' }, p('✗ ' + (i18n.pmCrypterBadKey || 'Your shared key is incorrect!'))) : null, input({ type: 'text', name: 'key', placeholder: 'd66d7d32f4d30f34812aee3d01347154', minlength: 32, maxlength: 32, size: 34, required: true, class: 'pm-crypter-key-input' }), button({ type: 'submit', class: 'pm-btn pm-fileshare-download' }, (i18n.fileShareDownload || 'Download').toUpperCase()) @@ -3259,6 +3441,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' ) } + if (subjectU === 'JOB_MATCH') { + return JobsBotCard({ sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } if (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') { return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } @@ -3277,6 +3462,9 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' if (subjectU === 'INDUSTRY_ADMITTED' || subjectU === 'INDUSTRY_APPLICATION' || subjectU === 'INDUSTRY_INVITED' || subjectU === 'INDUSTRY_DISSOLVED' || subjectU === 'INDUSTRY_BUILD_APPROVED' || subjectU === 'INDUSTRY_DISTRIBUTED') { return IndustryBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) } + if (subjectU === 'HOUSING_REQUESTED' || subjectU === 'HOUSING_CANCELLED' || subjectU === 'HOUSING_UNAVAILABLE') { + return HousingBotCard({ subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key, msgSize }) + } if (subjectU === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') { return ProjectPledgeCard({ sentAt, from: fromResolved, toLinks, content, text, key: msg.key, msgSize }) } @@ -3325,6 +3513,12 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' if (!threadOrder.length) return p({ class: 'empty' }, i18n.noPrivateMessages) + const threadTs = (tid) => (threadGroups[tid] || []).reduce((max, m) => { + const t = new Date(m?.value?.content?.sentAt || m.timestamp || 0).getTime() + return t > max ? t : max + }, 0) + threadOrder.sort((a, b) => threadTs(b) - threadTs(a)) + return threadOrder.map(tid => { const msgs = threadGroups[tid] const original = msgs[0] @@ -3355,44 +3549,6 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' ) } -exports.publishCustomView = async () => { - const action = "/publish/custom"; - const method = "post"; - - return template( - i18n.publishCustom, - section( - div({ class: "tags-header" }, - h2(i18n.publishCustom), - p(i18n.publishCustomDescription) - ), - form( - { action, method }, - textarea( - { - autofocus: true, - required: true, - name: "text", - rows: 10, - class: "textarea-full" - }, - "{\n", - ' "type": "feed",\n', - ' "hello": "world"\n', - "}" - ), - br(), - br(), - button({ type: "submit" }, i18n.submit) - ) - ), - section( - div({ class: "tags-header" }, - p(i18n.publishBasicInfo({ href: "/publish" })) - ) - ) - ); -}; exports.threadView = ({ messages, spreadMap = null }) => { const rootMessage = messages[0]; @@ -3416,69 +3572,23 @@ exports.threadView = ({ messages, spreadMap = null }) => { }`; }; -exports.publishView = (preview, text, contentWarning) => { +exports.likesView = async ({ messages, feed, name, spreadMap = null }) => { + const list = Array.isArray(messages) ? messages : []; return template( - i18n.publish, + i18n.viewLikes, section( div({ class: "tags-header" }, - h2(i18n.publishBlog), - p(i18n.publishLabel({ markdownUrl, linkTarget: "_blank" })) + h2(i18n.viewLikes), + p(userLink(feed, name)) ) ), - section( - div({ class: "publish-form" }, - form( - { - action: "/publish/preview", - method: "post", - enctype: "multipart/form-data", - }, - [ - label({ for: "contentWarning" }, i18n.blogSubject), - br(), - input({ - name: "contentWarning", - id: "contentWarning", - type: "text", - class: "contentWarning", - value: contentWarning || "", - placeholder: i18n.contentWarningPlaceholder - }), - br(), - label({ for: "text" }, i18n.blogMessage), - br(), - textarea( - { - required: true, - name: "text", - id: "text", - rows: "6", - cols: "50", - placeholder: i18n.publishWarningPlaceholder, - class: "publish-textarea", - maxlength: "7000" - }, - text || "" - ), - br(), - label({ for: "blob" }, i18n.blogImage || "Upload media (max-size: 50MB)"), - br(), - input({ type: "file", id: "blob", name: "blob" }), - br(), br(), - button({ type: "submit" }, i18n.blogPublish) - ] - ) - ) - ), - preview || "", - section( - div({ class: "tags-header" }, - p(i18n.publishCustomInfo({ href: "/publish/custom" })) - ) - ) + list.length + ? div(thread(list, spreadMap)) + : p({ class: "no-content" }, i18n.no_results) ); }; + //generate preview const ensureAt = (id) => { const s = String(id || "").trim() @@ -3679,44 +3789,6 @@ const generatePreview = ({ previewData, contentWarning, action }) => { ) } -exports.previewView = ({ previewData, contentWarning }) => { - const publishAction = "/publish" - const preview = generatePreview({ - previewData, - contentWarning, - action: publishAction, - }) - return exports.publishView(preview, (previewData && previewData.text) || "", contentWarning) -} - -const viewInfoBox = ({ viewTitle = null, viewDescription = null }) => { - if (!viewTitle && !viewDescription) { - return null - } - return section( - { class: "viewInfo" }, - viewTitle ? h1(viewTitle) : null, - viewDescription ? em(viewDescription) : null - ) -} -//generate preview - -exports.likesView = async ({ messages, feed, name, spreadMap = null }) => { - const authorLink = a( - { href: `/author/${encodeURIComponent(feed)}` }, - "@" + name - ); - const getSpread = (key) => (spreadMap instanceof Map ? spreadMap.get(key) : null) || null; - - return template( - ["@", name], - viewInfoBox({ - viewTitle: span(authorLink), - viewDescription: span(i18n.spreadedDescription) - }), - messages.map((msg) => post({ msg, spreadInfo: getSpread(msg.key) })) - ); -}; const messageListView = ({ messages, @@ -3741,71 +3813,10 @@ const messageListView = ({ ); }; -exports.popularView = ({ messages, prefix, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.popular), - p(i18n.popularDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.popular, - viewElements: [header, prefix], - spreadMap - }); -}; -exports.extendedView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.extended), - p(i18n.extendedDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.extended, - viewElements: header, - spreadMap - }); -}; -exports.latestView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.latest), - p(i18n.latestDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.latest, - viewElements: header, - spreadMap - }); -}; -exports.topicsView = ({ messages, prefix, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.topics), - p(i18n.topicsDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.topics, - viewElements: [header, prefix], - spreadMap - }); -}; -exports.summaryView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.summaries), - p(i18n.summariesDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.summaries, - viewElements: header, - aside: true, - spreadMap - }); -}; exports.spreadedView = ({ messages }) => { const header = div({ class: "tags-header" }, @@ -3819,19 +3830,6 @@ exports.spreadedView = ({ messages }) => { }); }; -exports.threadsView = ({ messages, spreadMap = null }) => { - const header = div({ class: "tags-header" }, - h2(i18n.threads), - p(i18n.threadsDescription) - ); - return messageListView({ - messages, - viewTitle: i18n.threads, - viewElements: header, - aside: true, - spreadMap - }); -}; exports.previewSubtopicView = async ({ previewData,