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,