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,