Forráskód Böngészése

Oasis release 0.6.8

psy 1 napja
szülő
commit
ad5b9d8f0f

+ 9 - 0
docs/CHANGELOG.md

@@ -13,6 +13,15 @@ All notable changes to this project will be documented in this file.
 ### Security
 -->
 
+## v0.6.8 - 2026-03-12
+
+### Fixed
+
+ + Government method image (Parliament plugin).
+ + Trending text formatting + tombstoned content (Trending plugin).
+ + Opinions text formatting + tombstoned content (Opinions plugin).
+ + Forums text formatting (Forums plugin).
+
 ## v0.6.7 - 2026-03-04
 
 ### Added

+ 1 - 1
src/backend/backend.js

@@ -1464,7 +1464,7 @@ router
   })
   .get('/forum', async ctx => {
     if (!checkMod(ctx, 'forumMod')) { ctx.redirect('/modules'); return; }
-    const filter = qf(ctx, 'recent'), forums = await forumModel.listAll(filter);
+    const filter = qf(ctx, 'hot'), forums = await forumModel.listAll(filter);
     ctx.body = await forumView(forums, filter);
   })
   .get('/forum/:forumId', async ctx => {

+ 92 - 2
src/backend/renderTextWithStyles.js

@@ -9,13 +9,57 @@ function getI18n() {
   }
 }
 
+function renderTextPreview(text, maxLength = 220) {
+  if (!text) return ''
+
+  let preview = String(text)
+
+  preview = preview
+    .replace(/```[\s\S]*?```/g, '')
+    .replace(/^>.*$/gm, '')
+    .replace(/^#{1,6}\s*/gm, '')
+    .replace(/^- /gm, '')
+    .replace(/^\d+\. /gm, '')
+    .replace(/\*\*(.*?)\*\*/g, '$1')
+    .replace(/\*(.*?)\*/g, '$1')
+    .replace(/`([^`]+)`/g, '$1')
+    .replace(/!\[.*?\]\(.*?\)/g, '')
+    .replace(/\[.*?\]\(.*?\)/g, '')
+    .replace(/\n+/g, ' ')
+    .trim()
+
+  if (preview.length > maxLength) {
+    preview = preview.slice(0, maxLength) + '...'
+  }
+
+  return preview
+}
+
 function renderTextWithStyles(text) {
   if (!text) return ''
   const i18n = getI18n()
-  return String(text)
+
+  let html = String(text)
+
+  html = html
     .replace(/&/g, '&')
     .replace(/</g, '&lt;')
     .replace(/>/g, '&gt;')
+
+  html = html
+    .replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>')
+    .replace(/^> (.*)$/gim, '<blockquote>$1</blockquote>')
+    .replace(/^---$/gim, '<hr/>')
+    .replace(/^### (.*)$/gim, '<h3>$1</h3>')
+    .replace(/^## (.*)$/gim, '<h2>$1</h2>')
+    .replace(/^# (.*)$/gim, '<h1>$1</h1>')
+
+  html = html
+    .replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
+    .replace(/\*(.*?)\*/gim, '<em>$1</em>')
+    .replace(/`([^`]+)`/gim, '<code>$1</code>')
+
+  html = html
     .replace(/!\[([^\]]*)\]\(\s*(&amp;[^)\s]+\.sha256)\s*\)/g, (_, alt, blob) =>
       `<img src="/blob/${encodeURIComponent(blob.replace(/&amp;/g, '&'))}" alt="${alt}" class="post-image" />`
     )
@@ -28,12 +72,16 @@ function renderTextWithStyles(text) {
     .replace(/\[pdf:([^\]]*)\]\(\s*(&amp;[^)\s]+\.sha256)\s*\)/g, (_, name, blob) =>
       `<a class="post-pdf" href="/blob/${encodeURIComponent(blob.replace(/&amp;/g, '&'))}" target="_blank">${name || i18n.pdfFallbackLabel || 'PDF'}</a>`
     )
+
+  html = html
     .replace(/\[@([^\]]+)\]\(@?([A-Za-z0-9+/=.\-]+\.ed25519)\)/g, (_, name, id) =>
       `<a href="/author/${encodeURIComponent('@' + id)}" class="mention" target="_blank">@${name}</a>`
     )
     .replace(/@([A-Za-z0-9+/=.\-]+\.ed25519)/g, (_, id) =>
       `<a href="/author/${encodeURIComponent('@' + id)}" class="mention" target="_blank">@${id}</a>`
     )
+
+  html = html
     .replace(/#(\w+)/g, (_, tag) =>
       `<a href="/hashtag/${encodeURIComponent(tag)}" class="styled-link" target="_blank">#${tag}</a>`
     )
@@ -43,6 +91,48 @@ function renderTextWithStyles(text) {
     .replace(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, email =>
       `<a href="mailto:${email}" class="styled-link">${email}</a>`
     )
+
+  const lines = html.split('\n')
+  let result = ''
+  let inUL = false
+  let inOL = false
+
+  for (let line of lines) {
+    if (/^- /.test(line)) {
+      if (!inUL) {
+        result += '<ul>'
+        inUL = true
+      }
+      result += `<li>${line.replace(/^- /, '')}</li>`
+      continue
+    }
+
+    if (/^\d+\. /.test(line)) {
+      if (!inOL) {
+        result += '<ol>'
+        inOL = true
+      }
+      result += `<li>${line.replace(/^\d+\. /, '')}</li>`
+      continue
+    }
+
+    if (inUL) {
+      result += '</ul>'
+      inUL = false
+    }
+
+    if (inOL) {
+      result += '</ol>'
+      inOL = false
+    }
+
+    result += line + '<br>'
+  }
+
+  if (inUL) result += '</ul>'
+  if (inOL) result += '</ol>'
+
+  return result
 }
 
-module.exports = { renderTextWithStyles }
+module.exports = { renderTextWithStyles, renderTextPreview }

+ 97 - 1
src/client/assets/styles/style.css

@@ -2370,6 +2370,103 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
   margin: 0.15em 0 0.2em 0;
   font-size: 1em;
   color: #ffd740;
+  line-height: 1.6;
+}
+.forum-body {
+  margin: 0.15em 0 0.2em 0;
+  font-size: 1em;
+  color: #ffd740;
+  line-height: 1.6;
+  word-break: break-word;
+}
+
+.forum-body h1 {
+  font-size: 1.6em;
+  margin: 10px 0 6px 0;
+  color: #ff9d00;
+}
+
+.forum-body h2 {
+  font-size: 1.4em;
+  margin: 8px 0 5px 0;
+  color: #ff9d00;
+}
+
+.forum-body h3 {
+  font-size: 1.25em;
+  margin: 6px 0 4px 0;
+  color: #ff9d00;
+}
+
+.forum-body ul,
+.forum-body ol {
+  padding-left: 22px;
+  margin: 6px 0;
+}
+
+.forum-body li {
+  margin: 3px 0;
+}
+
+.forum-body blockquote {
+  border-left: 3px solid #ff7300;
+  padding-left: 10px;
+  margin: 8px 0;
+  color: #ffb347;
+}
+
+.forum-body code {
+  background: #1a1a1a;
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-family: monospace;
+}
+
+.forum-body pre {
+  background: #111;
+  padding: 10px;
+  border-radius: 6px;
+  overflow-x: auto;
+  margin: 8px 0;
+}
+
+.forum-body hr {
+  border: none;
+  border-top: 1px solid #333;
+  margin: 10px 0;
+}
+
+.forum-body img {
+  max-width: 100%;
+  border-radius: 6px;
+  margin: 8px 0;
+}
+
+.forum-body video,
+.forum-body audio {
+  max-width: 100%;
+  margin: 8px 0;
+}
+
+.forum-score-box {
+  position: sticky;
+  top: 80px;
+}
+
+.forum-body.truncate {
+  max-height: 220px;
+  overflow: hidden;
+  position: relative;
+}
+
+.forum-body.truncate::after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  height: 60px;
+  background: linear-gradient(to bottom, rgba(0,0,0,0), #0f0f0f);
 }
 .forum-meta {
   font-size: 1em;
@@ -4093,7 +4190,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
 }
 
 .method-image-centered img {
-  max-width: 120px;
   height: auto;
   margin: 0 auto;
 }

+ 6 - 1
src/models/opinions_model.js

@@ -83,6 +83,7 @@ module.exports = ({ cooler }) => {
       if (!c) continue;
       if (c.type === 'tombstone' && c.target) {
         tombstoned.add(c.target);
+        byId.delete(c.target);
         continue;
       }
       if (c.opinions && !tombstoned.has(key) && !['task', 'event', 'report'].includes(c.type)) {
@@ -96,13 +97,17 @@ module.exports = ({ cooler }) => {
           }
         });
       }
+      if (c.type === 'feed' && !tombstoned.has(key) && !byId.has(key)) {
+        if (c.replaces) replaces.set(c.replaces, key);
+        byId.set(key, { key, value: { ...msg.value, content: c, preview: getPreview(c) } });
+      }
     }
 
     for (const replacedId of replaces.keys()) {
       byId.delete(replacedId);
     }
 
-    let filtered = Array.from(byId.values());
+    let filtered = Array.from(byId.values()).filter(m => validTypes.includes(m.value?.content?.type));
     const blobTypes = ['document', 'image', 'audio', 'video'];
     const blobCheckCache = new Map();
 

+ 7 - 1
src/models/trending_model.js

@@ -45,6 +45,7 @@ module.exports = ({ cooler }) => {
 
       if (c.type === 'tombstone' && c.target) {
         tombstoned.add(c.target);
+        itemsById.delete(c.target);
         continue;
       }
 
@@ -52,13 +53,18 @@ module.exports = ({ cooler }) => {
         if (c.replaces) replaces.set(c.replaces, k);
         itemsById.set(k, m);
       }
+
+      if (c.type === 'feed' && !tombstoned.has(k) && !itemsById.has(k)) {
+        if (c.replaces) replaces.set(c.replaces, k);
+        itemsById.set(k, m);
+      }
     }
 
     for (const replacedId of replaces.keys()) {
       itemsById.delete(replacedId);
     }
 
-    let rawItems = Array.from(itemsById.values());
+    let rawItems = Array.from(itemsById.values()).filter(m => types.includes(m.value?.content?.type));
     const blobTypes = ['document', 'image', 'audio', 'video'];
 
     let items = await Promise.all(

+ 1 - 1
src/server/package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.6.7",
+  "version": "0.6.8",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {

+ 1 - 1
src/server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.6.7",
+  "version": "0.6.8",
   "description": "Oasis Social Networking Project Utopia",
   "repository": {
     "type": "git",

+ 10 - 8
src/views/forum_view.js

@@ -6,6 +6,7 @@ const moment = require("../server/node_modules/moment");
 const { template, i18n } = require('./main_views');
 const { config } = require('../server/SSB_server.js');
 const { renderUrl } = require('../backend/renderUrl');
+const { renderTextWithStyles } = require('../backend/renderTextWithStyles');
 
 const userId = config.keys.id;
 const BASE_FILTERS = ['hot','all','mine','recent','top'];
@@ -181,7 +182,10 @@ const renderForumList = (forums, currentFilter) =>
                 href: `/forum/${encodeURIComponent(f.key)}`
               }, f.title)
             ),
-            div({ class: 'forum-body' }, ...renderUrl(f.text || '')),
+	    div({
+	      class: 'forum-body',
+	      innerHTML: renderTextWithStyles(f.text || '')
+	    }),
             div({ class: 'forum-meta' },
               span({ class: 'forum-positive-votes' },
                 `▲: ${f.positiveVotes || 0}`),
@@ -263,7 +267,7 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => {
         h2(i18n.forumTitle),
         p(i18n.forumDescription)
       ),
-      div({ class: 'mode-buttons' },
+       div({ class: 'mode-buttons-cols' },
         generateFilterButtons(BASE_FILTERS, currentFilter, '/forum', {
           hot: i18n.forumFilterHot,
           all: i18n.forumFilterAll,
@@ -313,12 +317,10 @@ exports.singleForumView = async (forum, messagesData, currentFilter) => {
               style: 'margin-left:12px;'
             }, forum.author)
           ),
-          div(
-            ...(forum.text || '').split('\n')
-              .map(l => l.trim())
-              .filter(l => l)
-              .map(l => p(...renderUrl(l)))
-          ),
+	  div({
+	    class: 'forum-body',
+	    innerHTML: renderTextWithStyles(forum.text || '')
+	  }),
           div({ class: 'forum-meta' },
             span({ class: 'votes-count' },
               `▲: ${messagesData.positiveVotes}`),

+ 8 - 6
src/views/opinions_view.js

@@ -21,7 +21,7 @@ const renderContentHtml = (content, key) => {
             span({ class: 'card-label' }, p(a({ href: content.url, target: '_blank', class: "bookmark-url" }, content.url)))
           ) : ""),
           content.lastVisit ? div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'),
+            span({ class: 'card-label' }, i18n.bookmarkLastVisitLabel + ':'),
             span({ class: 'card-value' }, new Date(content.lastVisit).toLocaleString())
           ) : "",
           content.description
@@ -143,11 +143,14 @@ const renderContentHtml = (content, key) => {
     case 'feed':
       return div({ class: 'opinion-feed' },
         div({ class: 'card-section feed' },
+          form({ method: "GET", action: `/feed/${encodeURIComponent(key)}` },
+            button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+          ),
+          br,
           div({ class: 'feed-text', innerHTML: sanitizeHtml(renderTextWithStyles(content.text)) }),
-          h2({ class: 'card-field' },
-            span({ class: 'card-label' }, `${i18n.tribeFeedRefeeds}: `),
-            span({ class: 'card-value' }, content.refeeds)
-          )
+          content.refeeds
+            ? h2({ class: 'card-field' }, span({ class: 'card-label' }, `${i18n.tribeFeedRefeeds}: `), span({ class: 'card-value' }, content.refeeds))
+            : ""
         )
       );
     case 'votes': {
@@ -219,7 +222,6 @@ const renderContentHtml = (content, key) => {
       return div({ class: 'styled-text' },
         div({ class: 'card-section styled-text-content' },
           div({ class: 'card-field' },
-            span({ class: 'card-label' }, i18n.textContentLabel + ':'),
             span({ class: 'card-value', innerHTML: sanitizeHtml(content.text || content.description || content.title || '[no content]') })
           )
         )

+ 8 - 3
src/views/trending_view.js

@@ -42,7 +42,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles) => {
         lastVisit
           ? div(
               { class: 'card-field' },
-              span({ class: 'card-label' }, i18n.bookmarkLastVisit + ':'),
+              span({ class: 'card-label' }, i18n.bookmarkLastVisitLabel + ':'),
               span({ class: 'card-value' }, new Date(lastVisit).toLocaleString())
             )
           : "",
@@ -115,8 +115,14 @@ const renderTrendingCard = (item, votes, categories, seenTitles) => {
     const { text, refeeds } = c;
     contentHtml = div({ class: 'trending-feed' },
       div({ class: 'card-section feed' },
+        form({ method: "GET", action: `/feed/${encodeURIComponent(item.key)}` },
+          button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
+        ),
+        br,
         div({ class: 'feed-text', innerHTML: sanitizeHtml(renderTextWithStyles(text)) }),
-        h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), span({ class: 'card-value' }, refeeds))
+        refeeds
+            ? h2({ class: 'card-field' }, span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '), span({ class: 'card-value' }, refeeds))
+            : ""
       )
     );
   } else if (c.type === 'votes') {
@@ -160,7 +166,6 @@ const renderTrendingCard = (item, votes, categories, seenTitles) => {
       div({ class: 'card-section styled-text-content' },
         div(
           { class: 'card-field' },
-          span({ class: 'card-label' }, i18n.textContentLabel + ':'),
           span({ class: 'card-value', innerHTML: sanitizeHtml(renderTextWithStyles(c.text || c.description || c.title || '[no content]')) })
         )
       )