فهرست منبع

Oasis release 0.3.6

psy 2 روز پیش
والد
کامیت
4719dd719d

+ 1 - 1
docs/PUB/deploy.md

@@ -30,7 +30,7 @@ Paste this:
     "level": "notice"
   },
   "caps": {
-    "shs": "+u5/ShHkb5g8jIWmybt/8ulGbZ2jFfzp8ggMwmKcRF0="
+    "shs": "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s="
   },
   "pub": true,
   "local": false,

+ 2 - 0
scripts/generate_shs.js

@@ -0,0 +1,2 @@
+const caps = require('ssb-caps');
+console.log('SHS (caps) Key:', caps.shs);

+ 85 - 38
src/backend/backend.js

@@ -197,27 +197,49 @@ const activityModel = require('../models/activity_model')({ cooler, isPublic: co
 const pixeliaModel = require('../models/pixelia_model')({ cooler, isPublic: config.public });
 const marketModel = require('../models/market_model')({ cooler, isPublic: config.public });
 
-function normalizeBlobId(id) {
-  if (!id.startsWith('&')) id = '&' + id;
-  if (!id.endsWith('.sha256')) id = id + '.sha256';
-  return id;
-}
-
-function renderBlobMarkdown(text, mentions = {}) {
-  return text
+// starting warmup
+about._startNameWarmup();
+
+async function renderBlobMarkdown(text, mentions = {}, myFeedId, myUsername) {
+  if (!text) return '';
+  const mentionByFeed = {};
+  Object.values(mentions).forEach(arr => {
+    arr.forEach(m => {
+      mentionByFeed[m.feed] = m;
+    });
+  });
+  text = text.replace(/\[@([^\]]+)\]\(([^)]+)\)/g, (_, name, id) => {
+    return `<a class="mention" href="/author/${encodeURIComponent(id)}">@${myUsername}</a>`;
+  });
+  const mentionRegex = /@([A-Za-z0-9_\-\.+=\/]+\.ed25519)/g;
+  const words = text.split(' ');
+
+  text = (await Promise.all(
+    words.map(async (word) => {
+      const match = mentionRegex.exec(word);
+      if (match && match[1]) {
+        const feedId = match[1];
+        if (feedId === myFeedId) {
+          return `<a class="mention" href="/author/${encodeURIComponent(feedId)}">@${myUsername}</a>`;
+        } 
+      }
+      return word;
+    })
+  )).join(' ');
+  text = text
     .replace(/!\[image:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-    `<img src="/blob/${encodeURIComponent(normalizeBlobId(id))}" alt="image" class="post-image" />`)
+      `<img src="/blob/${encodeURIComponent(id)}" alt="image" class="post-image" />`)
     .replace(/\[audio:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-    `<audio controls class="post-audio" src="/blob/${encodeURIComponent(normalizeBlobId(id))}"></audio>`)
+      `<audio controls class="post-audio" src="/blob/${encodeURIComponent(id)}"></audio>`)
     .replace(/\[video:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-    `<video controls class="post-video" src="/blob/${encodeURIComponent(normalizeBlobId(id))}"></video>`)
+      `<video controls class="post-video" src="/blob/${encodeURIComponent(id)}"></video>`)
     .replace(/\[pdf:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-    `<a class="post-pdf" href="/blob/${encodeURIComponent(normalizeBlobId(id))}" target="_blank">PDF</a>`)
-    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, id) =>
-    `<a class="post-link" href="/blob/${encodeURIComponent(normalizeBlobId(id))}">${label}</a>`);
+      `<a class="post-pdf" href="/blob/${encodeURIComponent(id)}" target="_blank">PDF</a>`);
+
+  return text;
 }
 
-let formattedTextCache = null;
+let formattedTextCache = null; 
 
 const preparePreview = async function (ctx) {
   let text = String(ctx.request.body.text || "");
@@ -228,7 +250,6 @@ const preparePreview = async function (ctx) {
     const token = m[2];
     const key = token;
     let found = mentions[key] || [];
-
     if (/\.ed25519$/.test(token)) {
       const name = await about.name(token);
       const img = await about.image(token);
@@ -271,7 +292,7 @@ const preparePreview = async function (ctx) {
     name: await about.name(ssbClient.id),
     image: await about.image(ssbClient.id),
   };
-  const renderedText = renderBlobMarkdown(text, mentions);
+  const renderedText = await renderBlobMarkdown(text, mentions, authorMeta.id, authorMeta.name);
   const hasBrTags = /<br\s*\/?>/.test(renderedText);
   const formattedText = formattedTextCache || (!hasBrTags ? renderedText.replace(/\n/g, '<br>') : renderedText);
   if (!formattedTextCache && !hasBrTags) {
@@ -282,7 +303,6 @@ const preparePreview = async function (ctx) {
   if (contentWarning && !finalContent.startsWith(contentWarning)) {
     finalContent = `<br>${finalContent}`;
   }
-
   return { authorMeta, text: renderedText, formattedText: finalContent, mentions };
 };
 
@@ -539,13 +559,13 @@ router
     ctx.body = await topicsView({ messages, prefix });
   })
   .get("/public/latest/summaries", async (ctx) => {
-  const summariesMod = ctx.cookies.get("summariesMod") || 'on';
-  if (summariesMod !== 'on') {
-    ctx.redirect('/modules');
-    return;
-  }
-  const messages = await post.latestSummaries();
-  ctx.body = await summaryView({ messages });
+    const summariesMod = ctx.cookies.get("summariesMod") || 'on';
+    if (summariesMod !== 'on') {
+      ctx.redirect('/modules');
+      return;
+    }
+    const messages = await post.latestSummaries();
+    ctx.body = await summaryView({ messages });
   })
   .get("/public/latest/threads", async (ctx) => {
     const threadsMod = ctx.cookies.get("threadsMod") || 'on';
@@ -741,8 +761,8 @@ router
   })
   .get('/agenda', async (ctx) => {
     const filter = ctx.query.filter || 'all';
-    const allItems = await agendaModel.listAgenda();
-    ctx.body = await agendaView(allItems, filter);
+    const data = await agendaModel.listAgenda(filter);
+    ctx.body = await agendaView(data, filter);
   })
   .get("/hashtag/:hashtag", async (ctx) => {
     const { hashtag } = ctx.params;
@@ -797,6 +817,9 @@ router
     const tribe = await tribesModel.getTribeById(tribeId);
     const userId = SSBconfig.config.keys.id;
     const query = ctx.query; 
+    if (!query.feedFilter) {
+      query.feedFilter = 'TOP';
+    }
     if (tribe.isAnonymous === false && !tribe.members.includes(userId)) {
       ctx.status = 403;
       ctx.body = { message: 'You cannot access to this tribe!' };
@@ -936,7 +959,6 @@ router
         ctx.body = buffer;
       }
     } catch (err) {
-      console.error("Image fetch error:", err);
       ctx.set("Content-Type", "image/png");
       ctx.body = await fakeImage();
     }
@@ -953,19 +975,34 @@ router
   })
   .get("/peers", async (ctx) => {
     const theme = ctx.cookies.get("theme") || config.theme;
-    const getMeta = async ({ theme }) => {
-      const peers = await meta.connectedPeers();
-      const peersWithNames = await Promise.all(
-        peers.map(async ([key, value]) => {
-          value.name = await about.name(value.key);
-          return [key, value];
-        })
-      );
+    const getMeta = async () => {
+      const allPeers = await meta.peers();
+      const connected = allPeers.filter(([, data]) => data.state === "connected");
+      const offline = allPeers.filter(([, data]) => data.state !== "connected");
+      const enrich = async (peers) => {
+        return await Promise.all(
+          peers.map(async ([address, data]) => {
+            const feedId = data.key || data.id;
+            const name = await about.name(feedId);
+            return [
+              address,
+              {
+                ...data,
+                key: feedId,
+                name: name || feedId,
+              },
+            ];
+          })
+        );
+      };
+      const connectedPeers = await enrich(connected);
+      const offlinePeers = await enrich(offline);
       return peersView({
-        peers: peersWithNames,
+        connectedPeers,
+        peers: offlinePeers,
       });
     };
-    ctx.body = await getMeta({ theme });
+    ctx.body = await getMeta();
   })
   .get("/invites", async (ctx) => {
     const theme = ctx.cookies.get("theme") || config.theme;
@@ -1499,6 +1536,16 @@ router
     await opinionsModel.createVote(contentId, category);
     ctx.redirect('/opinions');
   })
+  .post('/agenda/discard/:itemId', async (ctx) => {
+    const { itemId } = ctx.params;
+    await agendaModel.discardItem(itemId);
+    ctx.redirect('/agenda');
+  })
+  .post('/agenda/restore/:itemId', async (ctx) => {
+    const { itemId } = ctx.params;
+    await agendaModel.restoreItem(itemId);
+    ctx.redirect('/agenda?filter=discarded');
+  })
   .post('/feed/create', koaBody(), async ctx => {
     const { text } = ctx.request.body || {};
     await feedModel.createFeed(text.trim());

+ 1 - 1
src/backend/updater.js

@@ -110,7 +110,7 @@ exports.getRemoteVersion = async () => {
               printed = true; 
               console.log("\noasis@version: new code updates are available:\n\n1) Run Oasis and go to 'Settings' tab\n2) Click at 'Get updates' button to download latest code\n3) Restart Oasis when finished\n");
             } else {
-              console.log("\noasis@version: no updates requested.\n");
+              console.log("oasis@version: no updates requested.\n");
             }
           });
         }

+ 54 - 0
src/client/assets/styles/style.css

@@ -1522,3 +1522,57 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0;
   color: #ff6600;
 }
 
+/*avatar relationships*/
+.relationship-status {
+  display: flex;
+  flex-direction: row;
+  gap: 0.5rem;
+  justify-content: center;
+  margin: 0.8rem 0 0.5rem 0;
+}
+
+.status {
+  padding: 0.2rem 0.6rem;
+  border: 1px solid #555;
+  border-radius: 4px;
+  background-color: #2a2a2a;
+  font-size: 0.9rem;
+  color: #e0e0e0;
+}
+
+.status.mutual {
+  border-color: #557d3b;
+  color: #8bc34a;
+}
+
+.status.supporting {
+  border-color: #997f2e;
+  color: #f0c674;
+}
+
+.status.supported-by {
+  border-color: #3a6f9c;
+  color: #6ab0f3;
+}
+
+.status.blocked {
+  color: #ff5555;
+  border-color: #802020;
+}
+
+.status.blocked-by {
+  color: #ff8888;
+  border-color: #a04040;
+}
+
+.status.you {
+  padding: 0.2rem 0.6rem;
+  border: 1px solid #555;
+  border-radius: 4px;
+  background-color: #2a2a2a;
+  font-size: 0.9rem;
+  color: #FFDD44;
+  border-color: #FF6A00;
+  font-weight: bold;
+}
+

+ 8 - 3
src/client/assets/translations/oasis_en.js

@@ -130,11 +130,13 @@ module.exports = {
     beginningOfFeed: "This is the beginning of the feed",
     noNewerPosts: "No newer posts have been received yet.",
     relationshipNotFollowing: "You are not supported",
-    relationshipTheyFollow: "They support",
+    relationshipTheyFollow: "Supports you",
     relationshipMutuals: "Mutual support",
     relationshipFollowing: "You are supporting",
     relationshipYou: "You",
     relationshipBlocking: "You are blocking",
+    relationshipBlockedBy: "You are blocked",
+    relationshipMutualBlock: "Mutual block",
     relationshipNone: "You are not supporting",
     relationshipConflict: "Conflict",
     relationshipBlockingPost: "Blocked post",
@@ -224,6 +226,7 @@ module.exports = {
     recommended: "Recommended", 
     blocked: "Blocked",
     noConnections: "No peers connected.",
+    noDiscovered: "No peers discovered.",
     noSupportedConnections: "No peers supported.",
     noBlockedConnections: "No peers blocked.",
     noRecommendedConnections: "No peers recommended.",
@@ -1170,6 +1173,8 @@ module.exports = {
     agendaFilterTransfers: "TRANSFERS",
     agendaNoItems: "No assignments found.",
     agendaAuthor: "By",
+    agendaDiscardButton: "Discard",
+    agendaRestoreButton: "Restore",
     agendaCreatedAt: "Created At",
     agendaTitleLabel: "Title",
     agendaMembersCount: "Members",
@@ -1185,8 +1190,8 @@ module.exports = {
     agendaLARPLabel: "L.A.R.P.",
     agendaYes: "YES",
     agendaNo: "NO",
-    agendaInviteModeLabel: "Invite mode",
-    agendaAnonymousLabel: "Anonymous mode",
+    agendaInviteModeLabel: "Status",
+    agendaAnonymousLabel: "Anonymous",
     agendaMembersLabel: "Members",
     agendareportCategory: "Category",
     agendareportSeverity: "Severity",

+ 11 - 6
src/client/assets/translations/oasis_es.js

@@ -129,18 +129,20 @@ module.exports = {
     feedEmpty: "La red Oasis no ha visto nunca posts desde ésta cuenta.",
     beginningOfFeed: "Éste es el comienzo del feed",
     noNewerPosts: "No se han recibido posts nuevos, aún.",
-    relationshipNotFollowing: "No eres soportado",
-    relationshipTheyFollow: "Ellos soportan",
+    relationshipNotFollowing: "No te da soporte",
+    relationshipTheyFollow: "Te da soporte",
     relationshipMutuals: "Soporte mútuo",
-    relationshipFollowing: "Tu estás soportando",
+    relationshipFollowing: "Estás dando soporte",
     relationshipYou: "Tú",
-    relationshipBlocking: "Tu estás bloqueando",
-    relationshipNone: "Tu no estás soportando",
+    relationshipBlocking: "Estás bloqueando",
+    relationshipBlockedBy: "Te bloquea",
+    relationshipMutualBlock: "Bloqueo mutuo",
+    relationshipNone: "No estás dando soporte",
     relationshipConflict: "Conflicto",
     relationshipBlockingPost: "Post bloqueado",
     // spreads view
     viewLikes: "Ver soportes",
-    spreadedDescription: "Lista de posts soportado por éste habitante.",
+    spreadedDescription: "Lista de posts soportados por habitante.",
     // composer
     attachFiles: "Adjuntar ficheros",
     preview: "Previsualizar",
@@ -224,6 +226,7 @@ module.exports = {
     recommended: "Recomendado", 
     blocked: "Bloqueado",
     noConnections: "No hay nodos conectados.",
+    noDiscovered: "No has descubierto nodos.",
     noSupportedConnections: "No soportas nodos.",
     noBlockedConnections: "No bloqueas nodos.",
     noRecommendedConnections: "No recomiendas nodos.",
@@ -1168,6 +1171,8 @@ module.exports = {
     agendaFilterReports: "INFORMES",
     agendaFilterTransfers: "TRANSFERENCIAS",
     agendaNoItems: "No se encontraron asignaciones.",
+    agendaDiscardButton: "Descartar",
+    agendaRestoreButton: "Restaurar",
     agendaAuthor: "Por",
     agendaCreatedAt: "Creado el",
     agendaTitleLabel: "Título",

+ 10 - 5
src/client/assets/translations/oasis_eu.js

@@ -129,13 +129,15 @@ module.exports = {
     feedEmpty: "Oasis sareak ez du kontu honen bidalketarik ikusi inoiz.",
     beginningOfFeed: "Jarioaren hasiera da hau",
     noNewerPosts: "Ez da bidalketa berriagorik jaso oraindik.",
-    relationshipNotFollowing: "Ez duzu laguntzailerik",
-    relationshipTheyFollow: "Laguntzen ari dira",
-    relationshipMutuals: "Elkar laguntzen ari dira",
+    relationshipNotFollowing: "Ez zaitu laguntzen",
+    relationshipTheyFollow: "Laguntzen zaitu",
+    relationshipMutuals: "Elkar laguntzen",
     relationshipFollowing: "Laguntzen ari zara",
     relationshipYou: "Zu",
-    relationshipBlocking: "Blokeatzen ari zara",
-    relationshipNone: "Ez zara laguntzen ari",
+    relationshipBlocking: "Blokeatzen duzu",
+    relationshipBlockedBy: "Blokeatzen zaitu",
+    relationshipMutualBlock: "Elkar blokeatzen",
+    relationshipNone: "Ez duzu laguntzen",
     relationshipConflict: "Gatazka",
     relationshipBlockingPost: "Blokeatutako bidalketa",
     // spreads view
@@ -224,6 +226,7 @@ module.exports = {
     recommended: "Gomendatuta", 
     blocked: "Blokeatuta",
     noConnections: "Konektatutako parekorik ez.",
+    noDiscovered: "Ez duzu nodorik aurkitu.",
     noSupportedConnections: "Lagundutako parekorik ez.",
     noBlockedConnections: "Blokeatutako parekorik ez.",
     noRecommendedConnections: "Gomendatutako parekorik ez.",
@@ -1169,6 +1172,8 @@ module.exports = {
     agendaFilterReports: "TXOSTENAK",
     agendaFilterTransfers: "TRANSFERENTZIAK",
     agendaNoItems: "Esleipenik ez.",
+    agendaDiscardButton: "Baztertu",
+    agendaRestoreButton: "Berrezarri",
     agendaAuthor: "Nork",
     agendaCreatedAt: "Noiz",
     agendaTitleLabel: "Izenburua",

+ 3 - 0
src/configs/agenda-config.json

@@ -0,0 +1,3 @@
+{
+  "discardedItems": []
+}

+ 1 - 1
src/configs/server-config.json

@@ -3,7 +3,7 @@
     "level": "notice"
   },
   "caps": {
-    "shs": "+u5/ShHkb5g8jIWmybt/8ulGbZ2jFfzp8ggMwmKcRF0="
+    "shs": "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s="
   },
   "pub": false,
   "local": true,

+ 106 - 56
src/models/agenda_model.js

@@ -1,58 +1,60 @@
+const fs = require('fs');
+const path = require('path');
 const pull = require('../server/node_modules/pull-stream');
 const moment = require('../server/node_modules/moment');
 
+const agendaConfigPath = path.join(__dirname, '../configs/agenda-config.json');
+
+function readAgendaConfig() {
+  if (!fs.existsSync(agendaConfigPath)) {
+    fs.writeFileSync(agendaConfigPath, JSON.stringify({ discardedItems: [] }));
+  }
+  return JSON.parse(fs.readFileSync(agendaConfigPath));
+}
+
+function writeAgendaConfig(cfg) {
+  fs.writeFileSync(agendaConfigPath, JSON.stringify(cfg, null, 2));
+}
+
 module.exports = ({ cooler }) => {
   let ssb;
-
-  const openSsb = async () => {
-    if (!ssb) ssb = await cooler.open();
-    return ssb;
-  };
+  const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
 
   const fetchItems = (targetType, filterFn) =>
     new Promise((resolve, reject) => {
-      openSsb()
-        .then((ssbClient) => {
-          const userId = ssbClient.id;
-          pull(
-            ssbClient.createLogStream(),
-            pull.collect((err, msgs) => {
-              if (err) return reject(err);
-
-              const tombstoned = new Set();
-              const replacesMap = new Map();
-              const latestMap = new Map();
-
-              for (const msg of msgs) {
-                const c = msg.value?.content;
-                const k = msg.key;
-                if (!c) continue;
-
-                if (c.type === 'tombstone' && c.target) {
-                  tombstoned.add(c.target);
-                } else if (c.type === targetType) {
-                  if (c.replaces) replacesMap.set(c.replaces, k);
-                  latestMap.set(k, { key: k, value: msg.value });
-                }
-              }
-
-              for (const [oldId, newId] of replacesMap.entries()) {
-                latestMap.delete(oldId);
+      openSsb().then((ssbClient) => {
+        const userId = ssbClient.id;
+        pull(
+          ssbClient.createLogStream(),
+          pull.collect((err, msgs) => {
+            if (err) return reject(err);
+            const tombstoned = new Set();
+            const replacesMap = new Map();
+            const latestMap = new Map();
+            for (const msg of msgs) {
+              const c = msg.value?.content;
+              const k = msg.key;
+              if (!c) continue;
+              if (c.type === 'tombstone' && c.target) tombstoned.add(c.target);
+              else if (c.type === targetType) {
+                if (c.replaces) replacesMap.set(c.replaces, k);
+                latestMap.set(k, { key: k, value: msg.value });
               }
-
-              const results = Array.from(latestMap.values()).filter(
-                (msg) => !tombstoned.has(msg.key) && filterFn(msg.value.content, userId)
-              );
-
-              resolve(results.map(item => ({ ...item.value.content, id: item.key })));
-            })
-          );
-        })
-        .catch(reject);
+            }
+            for (const [oldId, newId] of replacesMap.entries()) latestMap.delete(oldId);
+            const results = Array.from(latestMap.values()).filter(
+              (msg) => !tombstoned.has(msg.key) && filterFn(msg.value.content, userId)
+            );
+            resolve(results.map(item => ({ ...item.value.content, id: item.key })));
+          })
+        );
+      }).catch(reject);
     });
 
   return {
     async listAgenda(filter = 'all') {
+      const agendaConfig = readAgendaConfig();
+      const discardedItems = agendaConfig.discardedItems || [];
       const ssbClient = await openSsb();
       const userId = ssbClient.id;
 
@@ -73,25 +75,73 @@ module.exports = ({ cooler }) => {
         ...marketItems.map(m => ({ ...m, type: 'market' })),
         ...reports.map(r => ({ ...r, type: 'report' }))
       ];
-
-      if (filter === 'tasks') combined = tasks;
-      else if (filter === 'events') combined = events;
-      else if (filter === 'transfers') combined = transfers;
-      else if (filter === 'tribes') combined = tribes.map(t => ({ ...t, type: 'tribe', title: t.name }));
-      else if (filter === 'market') combined = marketItems;
-      else if (filter === 'reports') combined = reports;
-      else if (filter === 'open') combined = combined.filter(i => i.status === 'OPEN');
-      else if (filter === 'closed') combined = combined.filter(i => i.status === 'CLOSED');
-
-      combined = Array.from(new Map(combined.map(i => [i.id, i])).values());
-
-      combined.sort((a, b) => {
+      const dedup = {};
+      for (const item of combined) {
+        const dA = item.startTime || item.date || item.deadline || item.createdAt;
+        if (!dedup[item.id]) dedup[item.id] = item;
+        else {
+          const existing = dedup[item.id];
+          const dB = existing.startTime || existing.date || existing.deadline || existing.createdAt;
+          if (new Date(dA) > new Date(dB)) dedup[item.id] = item;
+        }
+      }
+      combined = Object.values(dedup);
+
+      let filtered;
+      if (filter === 'discarded') {
+        filtered = combined.filter(i => discardedItems.includes(i.id));
+      } else {
+        filtered = combined.filter(i => !discardedItems.includes(i.id));
+
+        if (filter === 'tasks') filtered = filtered.filter(i => i.type === 'task');
+        else if (filter === 'events') filtered = filtered.filter(i => i.type === 'event');
+        else if (filter === 'transfers') filtered = filtered.filter(i => i.type === 'transfer');
+        else if (filter === 'tribes') filtered = filtered.filter(i => i.type === 'tribe');
+        else if (filter === 'market') filtered = filtered.filter(i => i.type === 'market');
+        else if (filter === 'reports') filtered = filtered.filter(i => i.type === 'report');
+        else if (filter === 'open') filtered = filtered.filter(i => i.status === 'OPEN');
+        else if (filter === 'closed') filtered = filtered.filter(i => i.status === 'CLOSED');
+      }
+
+      filtered.sort((a, b) => {
         const dateA = a.startTime || a.date || a.deadline || a.createdAt;
         const dateB = b.startTime || b.date || b.deadline || b.createdAt;
         return new Date(dateA) - new Date(dateB);
       });
 
-      return combined;
+      const mainItems = combined.filter(i => !discardedItems.includes(i.id));
+      const discarded = combined.filter(i => discardedItems.includes(i.id));
+
+      return {
+        items: filtered,
+        counts: {
+          all: mainItems.length,
+          open: mainItems.filter(i => i.status === 'OPEN').length,
+          closed: mainItems.filter(i => i.status === 'CLOSED').length,
+          tasks: mainItems.filter(i => i.type === 'task').length,
+          events: mainItems.filter(i => i.type === 'event').length,
+          transfers: mainItems.filter(i => i.type === 'transfer').length,
+          tribes: mainItems.filter(i => i.type === 'tribe').length,
+          market: mainItems.filter(i => i.type === 'market').length,
+          reports: mainItems.filter(i => i.type === 'report').length,
+          discarded: discarded.length
+        }
+      };
+    },
+
+    async discardItem(itemId) {
+      const agendaConfig = readAgendaConfig();
+      if (!agendaConfig.discardedItems.includes(itemId)) {
+        agendaConfig.discardedItems.push(itemId);
+        writeAgendaConfig(agendaConfig);
+      }
+    },
+
+    async restoreItem(itemId) {
+      const agendaConfig = readAgendaConfig();
+      agendaConfig.discardedItems = agendaConfig.discardedItems.filter(id => id !== itemId);
+      writeAgendaConfig(agendaConfig);
     }
   };
 };
+

+ 41 - 29
src/models/main_models.js

@@ -110,8 +110,8 @@ module.exports = ({ cooler, isPublic }) => {
     all_the_names = {};
 
     const allFeeds = Object.keys(feeds_to_name);
-    console.log(`Synced-feeds: [ ${allFeeds.length} ]`);
-    console.time("Sync-time");
+    console.log(` - Synced-feeds: [ ${allFeeds.length} ]`);
+    console.time(" - Sync-time");
 
     const lookups = [];
     for (const feed of allFeeds) {
@@ -123,7 +123,7 @@ module.exports = ({ cooler, isPublic }) => {
       .then(() => {
         dirty = false; 
         running = false;
-        console.timeEnd("Sync-time");
+        console.timeEnd(" - Sync-time");
       })
       .catch((err) => {
         running = false;
@@ -159,7 +159,7 @@ module.exports = ({ cooler, isPublic }) => {
     });
   };
   
-  //ABOUT MODEL
+//ABOUT MODEL
 models.about = {
   publicWebHosting: async (feedId) => {
     const result = await getAbout({
@@ -345,8 +345,8 @@ models.blob = {
   }
 };
 
-  //FRIENDS MODEL
-  models.friend = {
+//FRIENDS MODEL
+models.friend = {
   setRelationship: async ({ feedId, following, blocking }) => {
     if (following && blocking) {
       throw new Error("Cannot follow and block at the same time");
@@ -435,8 +435,8 @@ models.blob = {
     },
   };
   
-  //META MODEL
-  models.meta = {
+//META MODEL
+models.meta = {
     myFeedId: async () => {
       const ssb = await cooler.open();
       const { id } = ssb;
@@ -464,26 +464,20 @@ models.blob = {
     },
     peers: async () => {
       const ssb = await cooler.open();
-      const peersSource = await ssb.conn.peers();
-
       return new Promise((resolve, reject) => {
         pull(
-          peersSource,
+          ssb.conn.peers(),
           pull.take(1),
-          pull.collect((err, val) => {
+          pull.collect((err, [entries]) => {
             if (err) return reject(err);
-            resolve(val[0]);
+            resolve(entries);
           })
         );
       });
     },
     connectedPeers: async () => {
       const peers = await models.meta.peers();
-      return peers.filter(([address, data]) => {
-        if (data.state === "connected") {
-          return [address, data];
-        }
-      });
+      return peers.filter(([_, data]) => data.state === "connected");
     },
     connStop: async () => {
       const ssb = await cooler.open();
@@ -580,7 +574,7 @@ models.blob = {
     return conditions.every((x) => x === true);
   };
 
-  const maxMessages = 64;
+  const maxMessages = 30; // change it to control post overloading
 
   const getMessages = async ({
     myFeedId,
@@ -831,8 +825,8 @@ models.blob = {
     return messages.length ? messages[0] : undefined;
   };
 
-  // POST MODEL
-  const post = {
+// POST MODEL
+const post = {
     firstBy: async (feedId) => {
       return getLimitPost(feedId, false);
     },
@@ -879,28 +873,46 @@ models.blob = {
     const ssb = await cooler.open();
     const myFeedId = ssb.id;
     const { name: myUsername } = await getUserInfo(myFeedId);
-
     const query = [
-      {
+        {
         $filter: {
           "value.content.type": "post",
         },
       },
     ];
-
     const messages = await getMessages({
       myFeedId,
       customOptions,
       ssb,
       query,
       filter: (msg) => {
-        const mentionsText = lodash.get(msg, "value.content.text", "");
+      const content = msg.value.content;
+      if (content.mentions) {
+        if (Array.isArray(content.mentions)) {
+          if (content.mentions.some(m => {
+            return m.link === myFeedId || m.name === myUsername || m.name === '@' + myUsername;
+          })) {
+            return true; 
+          }
+        }
+        if (typeof content.mentions === 'object' && !Array.isArray(content.mentions)) {
+          const values = Object.values(content.mentions);
+          if (values.some(v => {
+            return v.link === myFeedId || v.name === myUsername || v.name === '@' + myUsername;
+          })) {
+            return true;
+            }
+          }
+        }
+        const mentionsText = lodash.get(content, "text", "");
         const mentionRegex = /<a class="mention" href="\/author\/([^"]+)">(@[^<]+)<\/a>/g;
         let match;
         while ((match = mentionRegex.exec(mentionsText))) {
-          if (match[1] === myFeedId || match[2] === myUsername) return true;
+          if (match[1] === myFeedId || match[2] === myUsername || match[2] === '@' + myUsername) {
+            return true; 
+          }
         }
-        return false;
+        return false; 
       },
     });
     return { messages, myFeedId };
@@ -1529,9 +1541,9 @@ models.blob = {
       const ssb = await cooler.open();
       if (image.length > 0) {
         const megabyte = Math.pow(2, 20);
-        const maxSize = 25 * megabyte;
+        const maxSize = 50 * megabyte;
         if (image.length > maxSize) {
-          throw new Error("File is too big, maximum size is 25 megabytes");
+          throw new Error("File is too big, maximum size is 50 megabytes");
         }
         return new Promise((resolve, reject) => {
           pull(

+ 2 - 3
src/server/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.3.5",
+  "version": "0.3.6",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@krakenslab/oasis",
-      "version": "0.3.5",
+      "version": "0.3.6",
       "hasInstallScript": true,
       "license": "AGPL-3.0",
       "dependencies": {
@@ -80,7 +80,6 @@
         "ssb-conn": "6.0.3",
         "ssb-conn-db": "^1.0.5",
         "ssb-conn-hub": "^1.2.0",
-        "ssb-conn-query": "^1.2.2",
         "ssb-conn-staging": "^1.0.0",
         "ssb-db": "^20.4.1",
         "ssb-device-address": "^1.1.6",

+ 1 - 2
src/server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.3.5",
+  "version": "0.3.6",
   "description": "Oasis Social Networking Project Utopia",
   "repository": {
     "type": "git",
@@ -94,7 +94,6 @@
     "ssb-conn": "6.0.3",
     "ssb-conn-db": "^1.0.5",
     "ssb-conn-hub": "^1.2.0",
-    "ssb-conn-query": "^1.2.2",
     "ssb-conn-staging": "^1.0.0",
     "ssb-db": "^20.4.1",
     "ssb-device-address": "^1.1.6",

+ 6 - 4
src/server/ssb_metadata.js

@@ -47,17 +47,19 @@ async function printMetadata(mode, modeColor = colors.cyan) {
   const publicKey = config.keys?.public || '';
 
   console.log("=========================");
-  console.log(`Mode: ${modeColor}${mode}${colors.reset}`);
+  console.log(`Running mode: ${modeColor}${mode}${colors.reset}`);
   console.log("=========================");
-  console.log(`Package: ${colors.blue}${name} ${colors.yellow}[Version: ${version}]${colors.reset}`);
-  console.log("Logging Level:", logLevel);
-  console.log(`Oasis ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`);
+  console.log(`- Package: ${colors.blue}${name} ${colors.yellow}[Version: ${version}]${colors.reset}`);
+  console.log("- Logging Level:", logLevel);
+  console.log(`- Oasis ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`);
+  console.log("");
   console.log("=========================");
   console.log("Modules loaded: [", modules.length, "]");
   console.log("=========================");
 
   // Check for updates
   await checkForUpdate();
+  console.log("=========================");
 }
 
 module.exports = {

+ 62 - 83
src/views/agenda_view.js

@@ -5,31 +5,8 @@ const { config } = require('../server/SSB_server.js');
 
 userId = config.keys.id;
 
-exports.agendaView = async (items, filter) => {
-  const now = Date.now();
-  const counts = {
-    all: items.length,
-    open: items.filter(i => i.status === 'OPEN').length,
-    closed: items.filter(i => i.status === 'CLOSED').length,
-    tasks: items.filter(i => i.type === 'task').length,
-    events: items.filter(i => i.type === 'event').length,
-    transfers: items.filter(i => i.type === 'transfer').length,
-    tribes: items.filter(i => i.type === 'tribe').length,
-    market: items.filter(i => i.type === 'market').length,
-    reports: items.filter(i => i.type === 'report').length
-  };
-
-  const filtered =
-    filter === 'open' ? items.filter(i => i.status === 'OPEN') :
-    filter === 'closed' ? items.filter(i => i.status === 'CLOSED') :
-    filter === 'tasks' ? items.filter(i => i.type === 'task') :
-    filter === 'events' ? items.filter(i => i.type === 'event') :
-    filter === 'transfers' ? items.filter(i => i.type === 'transfer') :
-    filter === 'tribes' ? items.filter(i => i.type === 'tribe') :
-    filter === 'market' ? items.filter(i => i.type === 'market') :
-    filter === 'reports' ? items.filter(i => i.type === 'report') :
-    items;
-
+exports.agendaView = async (data, filter) => {
+  const { items, counts } = data;
   const fmt = d => moment(d).format('YYYY/MM/DD HH:mm:ss');
 
   return template(
@@ -56,14 +33,16 @@ exports.agendaView = async (items, filter) => {
           button({ type: 'submit', name: 'filter', value: 'tribes', class: filter === 'tribes' ? 'filter-btn active' : 'filter-btn' },
             `${i18n.agendaFilterTribes} (${counts.tribes})`),
           button({ type: 'submit', name: 'filter', value: 'market', class: filter === 'market' ? 'filter-btn active' : 'filter-btn' },
-            `${i18n.agendaFilterMarket} (${counts.market})`), 
+            `${i18n.agendaFilterMarket} (${counts.market})`),
           button({ type: 'submit', name: 'filter', value: 'transfers', class: filter === 'transfers' ? 'filter-btn active' : 'filter-btn' },
-            `${i18n.agendaFilterTransfers} (${counts.transfers})`)
+            `${i18n.agendaFilterTransfers} (${counts.transfers})`),
+          button({ type: 'submit', name: 'filter', value: 'discarded', class: filter === 'discarded' ? 'filter-btn active' : 'filter-btn' },
+            `DISCARDED (${counts.discarded})`)
         )
       ),
       div({ class: 'agenda-list' },
-        filtered.length
-          ? filtered.map(item => {
+        items.length
+          ? items.map(item => {
               const author = item.seller || item.organizer || item.from || item.author;
               const commonFields = [
                 p(`${i18n.agendaAuthor}: `, a({ href: `/author/${encodeURIComponent(author)}` }, author)),
@@ -71,64 +50,69 @@ exports.agendaView = async (items, filter) => {
               ];
               let details = [];
               let actionButton = null;
-		if (item.type === 'market') {
-		  commonFields.push(p(`${i18n.marketItemType}: ${item.item_type}`));
-		  commonFields.push(p(`${i18n.marketItemTitle}: ${item.title}`));
-		  commonFields.push(p(`${i18n.marketItemDescription}: ${item.description}`));
-		  commonFields.push(p(`${i18n.marketItemPrice}: ${item.price} ECO`));
-		  commonFields.push(p(`${i18n.marketItemIncludesShipping}: ${item.includesShipping ? i18n.agendaYes : i18n.agendaNo}`));
-		  commonFields.push(p(`${i18n.marketItemSeller}: `, a({ href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)));
-		  commonFields.push(p(`${i18n.marketItemAvailable}: ${moment(item.createdAt).format('YYYY-MM-DD HH:mm')}`));
-		  commonFields.push(
-		    item.image
-		      ? img({ src: `/blob/${encodeURIComponent(item.image)}`, class: 'market-image' })
-		      : p(i18n.marketNoImage)
-		  );
-		  commonFields.push(
-		    item.tags && item.tags.length
-		      ? div(
-			  item.tags.map(tag =>
-			    a(
-			      {
-				href: `/search?query=%23${encodeURIComponent(tag)}`,
-				class: 'tag-link',
-				style: 'margin-right:0.8em;',
-			      },
-			      `#${tag}`
-			    )
-			  )
-			)
-		      : null
-		  );
-
-		  if (item.item_type === 'auction') {
-		    details.push(p(`${i18n.marketItemAvailable}: ${moment(item.deadline).format('YYYY-MM-DD HH:mm')}`));
-		    const bids = item.auctions_poll.map(bid => parseFloat(bid.split(':')[1]));
-		    const maxBid = bids.length ? Math.max(...bids) : 0;
-		    details.push(p(`${i18n.marketItemHighestBid}: ${maxBid} ECO`));
-		  }
+              if (filter === 'discarded') {
+		actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}` },
+		  button({ type: 'submit', class: 'restore-btn' }, i18n.agendaRestoreButton)
+		);
+              } else {
+		actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}` },
+		  button({ type: 'submit', class: 'discard-btn' }, i18n.agendaDiscardButton)
+		);
+              }
 
-		  details.push(p(`${i18n.marketItemStatus}: ${item.status}`));
-		}
+              if (item.type === 'market') {
+                commonFields.push(p(`${i18n.marketItemType}: ${item.item_type}`));
+                commonFields.push(p(`${i18n.marketItemTitle}: ${item.title}`));
+                commonFields.push(p(`${i18n.marketItemDescription}: ${item.description}`));
+                commonFields.push(p(`${i18n.marketItemPrice}: ${item.price} ECO`));
+                commonFields.push(p(`${i18n.marketItemIncludesShipping}: ${item.includesShipping ? i18n.agendaYes : i18n.agendaNo}`));
+                commonFields.push(p(`${i18n.marketItemSeller}: `, a({ href: `/author/${encodeURIComponent(item.seller)}` }, item.seller)));
+                commonFields.push(p(`${i18n.marketItemAvailable}: ${moment(item.createdAt).format('YYYY-MM-DD HH:mm')}`));
+                commonFields.push(
+                  item.image
+                    ? img({ src: `/blob/${encodeURIComponent(item.image)}`, class: 'market-image' })
+                    : p(i18n.marketNoImage)
+                );
+                commonFields.push(
+                  item.tags && item.tags.length
+                    ? div(
+                        item.tags.map(tag =>
+                          a(
+                            {
+                              href: `/search?query=%23${encodeURIComponent(tag)}`,
+                              class: 'tag-link',
+                              style: 'margin-right:0.8em;',
+                            },
+                            `#${tag}`
+                          )
+                        )
+                      )
+                    : null
+                );
+                if (item.item_type === 'auction') {
+                  details.push(p(`${i18n.marketItemAvailable}: ${moment(item.deadline).format('YYYY-MM-DD HH:mm')}`));
+                  const bids = item.auctions_poll.map(bid => parseFloat(bid.split(':')[1]));
+                  const maxBid = bids.length ? Math.max(...bids) : 0;
+                  details.push(p(`${i18n.marketItemHighestBid}: ${maxBid} ECO`));
+                }
+                details.push(p(`${i18n.marketItemStatus}: ${item.status}`));
+              }
               if (item.type === 'tribe') {
                 commonFields.push(p(`${i18n.agendaDescriptionLabel}: ${item.description || i18n.noDescription}`));
                 details = [
                   p(`${i18n.agendaMembersCount}: ${item.members.length || 0}`),
                   p(`${i18n.agendaLocationLabel}: ${item.location || i18n.noLocation}`),
-                  p(`${i18n.agendaLARPLabel}: ${item.isLARP ? i18n.agendaYes : i18n.agendaNo}`), 
-                  p(`${i18n.agendaAnonymousLabel}: ${item.isAnonymous ? i18n.agendaYes : i18n.agendaNo}`), 
+                  p(`${i18n.agendaLARPLabel}: ${item.isLARP ? i18n.agendaYes : i18n.agendaNo}`),
+                  p(`${i18n.agendaAnonymousLabel}: ${item.isAnonymous ? i18n.agendaYes : i18n.agendaNo}`),
                   p(`${i18n.agendaInviteModeLabel}: ${item.inviteMode || i18n.noInviteMode}`)
                 ];
-
-                const membersList = item.members.map(member => 
+                const membersList = item.members.map(member =>
                   p(a({ href: `/author/${encodeURIComponent(member)}` }, member))
                 );
-
                 details.push(
                   div({ class: 'members-list' }, `${i18n.agendaMembersLabel}:`, membersList)
                 );
               }
-
               if (item.type === 'report') {
                 details = [
                   p(`${i18n.agendareportCategory}: ${item.category || i18n.noCategory}`),
@@ -137,7 +121,6 @@ exports.agendaView = async (items, filter) => {
                   p(`${i18n.agendareportDescription}: ${item.description || i18n.noDescription}`)
                 ];
               }
-
               if (item.type === 'event') {
                 details = [
                   p(`${i18n.eventDescriptionLabel}: ${item.description}`),
@@ -146,12 +129,9 @@ exports.agendaView = async (items, filter) => {
                   p(`${i18n.eventPriceLabel}: ${item.price} ECO`),
                   p(`${i18n.eventUrlLabel}: ${item.url || i18n.noUrl}`)
                 ];
-                actionButton = form({ method: 'POST', action: `/events/attend/${encodeURIComponent(item.id)}` },
-                              button({ type: 'submit', class: 'assign-btn' },
-                                     `${i18n.eventAttendButton}`)
-                );
+                actionButton = actionButton || form({ method: 'POST', action: `/events/attend/${encodeURIComponent(item.id)}` },
+                  button({ type: 'submit', class: 'assign-btn' }, `${i18n.eventAttendButton}`));
               }
-
               if (item.type === 'task') {
                 details = [
                   p(`${i18n.taskDescriptionLabel}: ${item.description}`),
@@ -159,11 +139,10 @@ exports.agendaView = async (items, filter) => {
                   p(`${i18n.taskLocationLabel}: ${item.location}`)
                 ];
                 const assigned = Array.isArray(item.assignees) && item.assignees.includes(userId);
-                actionButton = form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(item.id)}` },
-                              button({ type: 'submit', class: 'assign-btn' },
-                                     assigned ? i18n.taskUnassignButton : i18n.taskAssignButton));
+                actionButton = actionButton || form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(item.id)}` },
+                  button({ type: 'submit', class: 'assign-btn' },
+                    assigned ? i18n.taskUnassignButton : i18n.taskAssignButton));
               }
-
               if (item.type === 'transfer') {
                 details = [
                   p(`${i18n.agendaTransferConcept}: ${item.concept}`),

+ 10 - 4
src/views/indexing_view.js

@@ -1,6 +1,12 @@
-const { html,  title, link, meta,  body, main, p, progress } = require("../server/node_modules/hyperaxe");
+const { html, head, title, link, meta, body, main, p, progress } = require("../server/node_modules/hyperaxe");
 const { i18n } = require('./main_views');
 
+const doctypeString = '<!DOCTYPE html>';
+
+function toAttributes(attrs) {
+  return Object.entries(attrs).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(', ');
+}
+
 exports.indexingView = ({ percent }) => {
   const message = `Oasis has only processed ${percent}% of the messages and needs to catch up. This page will refresh every 10 seconds. Thanks for your patience! ❤`;
   const nodes = html(
@@ -15,7 +21,7 @@ exports.indexingView = ({ percent }) => {
       }),
       meta({
         name: "viewport",
-        content: toAttributes({ width: "device-width", "initial-scale": 1 }),
+        content: "width=device-width, initial-scale=1",
       }),
       meta({ "http-equiv": "refresh", content: 10 })
     ),
@@ -27,6 +33,6 @@ exports.indexingView = ({ percent }) => {
       )
     )
   );
-  const result = doctypeString + nodes.outerHTML;
-  return result;
+  return doctypeString + nodes.outerHTML;
 };
+

+ 77 - 51
src/views/main_views.js

@@ -42,23 +42,6 @@ exports.selectedLanguage = selectedLanguage;
 
 //markdown
 const markdownUrl = "https://commonmark.org/help/";
-function renderBlobMarkdown(text, mentions = {}) {
-  text = text
-    .replace(/!\[image:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-      `<img src="/blob/${encodeURIComponent(id)}" alt="image" class="post-image" />`)
-    .replace(/\[audio:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-      `<audio controls class="post-audio" src="/blob/${encodeURIComponent(id)}"></audio>`)
-    .replace(/\[video:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-      `<video controls class="post-video" src="/blob/${encodeURIComponent(id)}"></video>`)
-    .replace(/\[pdf:[^\]]+\]\(([^)]+)\)/g, (_, id) =>
-      `<a class="post-pdf" href="/blob/${encodeURIComponent(id)}" target="_blank">PDF</a>`)
-    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, id) =>
-      `<a class="post-link" href="/blob/${encodeURIComponent(id)}">${label}</a>`)
-    .replace(/\[@([^\]]+)\]\(([^)]+)\)/g, (_, name, id) =>
-      `<a class="mention" href="/author/${encodeURIComponent(id)}">@${name}</a>`);
-
-  return text;
-}
 
 const doctypeString = "<!DOCTYPE html>";
 
@@ -389,7 +372,7 @@ const template = (titlePrefix, ...elements) => {
         { class: "header" },
         div(
           { class: "top-bar-left" },
-          a({ class: "logo-icon", href: "https://solarnethub.com" },
+          a({ class: "logo-icon", href: "/" },
             img({ class: "logo-icon", src: "/assets/images/snh-oasis.jpg", alt: "Oasis Logo" })
           ),
           nav(
@@ -868,40 +851,83 @@ exports.authorView = ({
     }
   }
 
-  const relationshipText = (() => {
-    if (relationship.me === true) return i18n.relationshipYou;
-    if (relationship.following === true && relationship.blocking === false) return i18n.relationshipFollowing;
-    if (relationship.following === false && relationship.blocking === true) return i18n.relationshipBlocking;
-    if (relationship.following === false && relationship.blocking === false) return i18n.relationshipNone;
-    if (relationship.following === true && relationship.blocking === true) return i18n.relationshipConflict;
-    throw new Error(`Unknown relationship ${JSON.stringify(relationship)}`);
-  })();
-
-  const prefix = section(
-    { class: "message" },
-    div(
-      { class: "profile" },
-      div({ class: "avatar-container" },
-        img({ class: "avatar", src: avatarUrl }),
-        h1({ class: "name" }, name)
-      ),
-      pre({
-        class: "md-mention",
-        innerHTML: markdownMention,
-      })
+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 messages = [];
+  messages.push(
+    following
+      ? i18n.relationshipFollowing
+      : i18n.relationshipNone
+  );
+  messages.push(
+    followsMe
+      ? i18n.relationshipTheyFollow
+      : i18n.relationshipNotFollowing
+  );
+  return messages.join(". ") + ".";
+})();
+
+const prefix = section(
+  { class: "message" },
+  div(
+    { class: "profile" },
+    div({ class: "avatar-container" },
+      img({ class: "avatar", src: avatarUrl }),
+      h1({ class: "name" }, name),
     ),
-    description !== "" ? article({ innerHTML: markdown(description) }) : null,
-    footer(
-      div(
-        { class: "profile" },
-        ...contactForms.map(form => span({ style: "font-weight: bold;" }, form)),
-        span(nbsp, relationshipText),
-        relationship.me
-          ? a({ href: `/profile/edit`, class: "btn" }, nbsp, i18n.editProfile)
-          : span(i18n.relationshipNotFollowing),
-        a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes)
-      )
+    pre({
+      class: "md-mention",
+      innerHTML: markdownMention,
+    })
+  ),
+  description !== "" ? article({ innerHTML: markdown(description) }) : null,
+  footer(
+  div(
+    { class: "profile" },
+    ...contactForms.map(form => span({ style: "font-weight: bold;" }, form)),
+    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
+                  )
+               ]
+            ]
+        )
+    ),
+    relationship.me
+      ? a({ href: `/profile/edit`, class: "btn" }, nbsp, i18n.editProfile)
+      : null,
+    a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes)
     )
+   )
   );
 
   const linkUrl = relationship.me
@@ -1404,7 +1430,7 @@ const generatePreview = ({ previewData, contentWarning, action }) => {
               {
                 href: `/author/@${encodeURIComponent(matches[0].feed)}`,
               },
-              `@${matches[0].name}`
+              `@${authorMeta.name}`
             )
           ),
           div(

+ 20 - 40
src/views/peers_view.js

@@ -1,38 +1,19 @@
-const { form, button, div, h2, p, section, ul, li, a, br } = require("../server/node_modules/hyperaxe");
-const { template, i18n } = require('./main_views');
-
-const peersView = async ({ peers }) => {
-  const startButton = form(
-    { action: "/settings/conn/start", method: "post" },
-    button({ type: "submit" }, i18n.startNetworking)
-  );
-
-  const restartButton = form(
-    { action: "/settings/conn/restart", method: "post" },
-    button({ type: "submit" }, i18n.restartNetworking)
-  );
-
-  const stopButton = form(
-    { action: "/settings/conn/stop", method: "post" },
-    button({ type: "submit" }, i18n.stopNetworking)
-  );
-
-  const syncButton = form(
-    { action: "/settings/conn/sync", method: "post" },
-    button({ type: "submit" }, i18n.sync)
-  );
-
+const peersView = async ({ peers, connectedPeers }) => {
+  const { form, button, div, h2, p, section, ul, li, a, br } = require("../server/node_modules/hyperaxe");
+  const { template, i18n } = require('./main_views');
+
+  const startButton = form({ action: "/settings/conn/start", method: "post" }, button({ type: "submit" }, i18n.startNetworking));
+  const restartButton = form({ action: "/settings/conn/restart", method: "post" }, button({ type: "submit" }, i18n.restartNetworking));
+  const stopButton = form({ action: "/settings/conn/stop", method: "post" }, button({ type: "submit" }, i18n.stopNetworking));
+  const syncButton = form({ action: "/settings/conn/sync", method: "post" }, button({ type: "submit" }, i18n.sync));
   const connButtons = [startButton, restartButton, stopButton, syncButton];
-
-  const connectedPeers = (peers || []).filter(([, data]) => data.state === "connected");
-
-  const peerList = connectedPeers.map(([, data]) =>
-    li(
-      data.name, br,
-      a({ href: `/author/${encodeURIComponent(data.key)}` }, data.key), br, br
-    )
-  );
-
+  const renderPeerList = (list) =>
+    list.map(([, data]) =>
+      li(
+        data.name, br,
+        a({ href: `/author/${encodeURIComponent(data.key)}` }, data.key), br, br
+      )
+    );
   return template(
     i18n.peers,
     section(
@@ -40,14 +21,13 @@ const peersView = async ({ peers }) => {
         h2(i18n.peers),
         p(i18n.peerConnectionsIntro)
       ),
-      div({ class: "conn-actions" },
-        ...connButtons
-      ),
+      div({ class: "conn-actions" }, ...connButtons),
       div({ class: "peers-list" },
         h2(`${i18n.online} (${connectedPeers.length})`),
-        connectedPeers.length > 0
-          ? ul(peerList)
-          : p(i18n.noConnections),
+        connectedPeers.length > 0 ? ul(renderPeerList(connectedPeers)) : p(i18n.noConnections),
+        br(),
+        h2(`${i18n.offline} (${peers.length})`),
+        peers.length > 0 ? ul(renderPeerList(peers)) : p(i18n.noDiscovered),
         p(i18n.connectionActionIntro)
       )
     )

+ 1 - 5
src/views/stats_view.js

@@ -108,11 +108,7 @@ exports.statsView = (stats, filter) => {
             ])
             : div({ class: 'stats-container' }, [
               div({ style: blockStyle },
-                h2(`${i18n.TOMBSTONEButton}: ${stats.userTombstoneCount}`),
-                ul(
-                  li(`${i18n.statsNetwork}: ${stats.networkTombstoneCount}`),
-                  li(`${i18n.statsYou}: ${stats.userTombstoneCount}`)
-                )
+                h2(`${i18n.TOMBSTONEButton}: ${stats.userTombstoneCount}`)
               )
             ])
       )

+ 4 - 4
src/views/tribes_view.js

@@ -28,7 +28,7 @@ const renderPaginationTribesView = (page, totalPages, filter) => {
 
 const renderFeedTribesView = (tribe, page, query, filter) => {
   const feed = Array.isArray(tribe.feed) ? tribe.feed : [];
-  const feedFilter = (query.feedFilter || 'RECENT').toUpperCase();
+  const feedFilter = (query.feedFilter || 'TOP').toUpperCase();
   let filteredFeed = feed;
 
   if (feedFilter === 'MINE') filteredFeed = feed.filter(m => m.author === userId);
@@ -53,7 +53,7 @@ const renderFeedTribesView = (tribe, page, query, filter) => {
 
   return div({ class: 'tribe-feed' },
     div({ class: 'feed-actions', style: 'margin-bottom:8px;' },
-      ['RECENT', 'MINE', 'ALL', 'TOP'].map(f =>
+      ['TOP', 'MINE', 'ALL', 'RECENT'].map(f =>
         form({ method: 'GET', action: '/tribes' },
           input({ type: 'hidden', name: 'filter', value: filter }),
           input({ type: 'hidden', name: 'feedFilter', value: f }),
@@ -335,7 +335,7 @@ const renderFeedTribeView = async (tribe, query = {}, filter) => {
   }
   return div({ class: 'tribe-feed-full' },
     div({ class: 'feed-actions', style: 'margin-bottom:8px;' },
-      ['RECENT', 'MINE', 'ALL', 'TOP'].map(f =>
+      ['TOP', 'MINE', 'ALL', 'RECENT'].map(f =>
 	form({ method: 'GET', action: `/tribe/${encodeURIComponent(tribe.id)}` },
 	  input({ type: 'hidden', name: 'filter', value: filter }),
 	  input({ type: 'hidden', name: 'feedFilter', value: f }), 
@@ -368,7 +368,7 @@ exports.tribeView = async (tribe, userId, query) => {
   if (!tribe) {
     return div({ class: 'error' }, 'Tribe not found!');
   }
-  const feedFilter = (query.feedFilter || 'RECENT').toUpperCase();
+  const feedFilter = (query.feedFilter || 'TOP').toUpperCase();
   const imageSrc = tribe.image
     ? `/blob/${encodeURIComponent(tribe.image)}`
     : '/assets/images/default-tribe.png';