Browse Source

Oasis release 0.5.3

psy 1 day ago
parent
commit
7f45df88ca
3 changed files with 78 additions and 32 deletions
  1. 1 1
      docs/CHANGELOG.md
  2. 45 27
      src/models/activity_model.js
  3. 32 4
      src/models/stats_model.js

+ 1 - 1
docs/CHANGELOG.md

@@ -17,7 +17,7 @@ All notable changes to this project will be documented in this file.
 
 ### Fixed
 
- + Tribes duplication (Tribes plugin).
+ + Tribes duplication (Tribes plugin + Activity plugin + Stats plugin).
 
 ## v0.5.2 - 2025-10-22
 

+ 45 - 27
src/models/activity_model.js

@@ -111,54 +111,72 @@ module.exports = ({ cooler }) => {
             }
           };
           idToAction.set(ev.id, augmented);
-          idToTipId.set(ev.id, ev.id); 
+          idToTipId.set(ev.id, ev.id);
         }
       }
 
       const latest = [];
-	for (const a of idToAction.values()) {
-	  if (tombstoned.has(a.id)) continue;
-	  const c = a.content || {};
-	  if (c.root && tombstoned.has(c.root)) continue;
-	  if (a.type === 'vote' && tombstoned.has(c.vote?.link)) continue;
-	  if (c.key && tombstoned.has(c.key)) continue;
-	  if (c.branch && tombstoned.has(c.branch)) continue;
-	  if (c.target && tombstoned.has(c.target)) continue;
-	  if (a.type === 'document') {
-	    const url = c.url;
-	    const ok = await hasBlob(ssbClient, url);
-	    if (!ok) continue;
-	  }
-	  if (a.type === 'forum' && c.root) {
-	    const rootId = typeof c.root === 'string' ? c.root : (c.root?.key || c.root?.id || '');
-	    const rootAction = idToAction.get(rootId);
-	    a.content.rootTitle = rootAction?.content?.title || a.content.rootTitle || '';
-	    a.content.rootKey = rootId || a.content.rootKey || '';
-	  }
-	  latest.push({ ...a, tipId: idToTipId.get(a.id) || a.id });
-	}
+      for (const a of idToAction.values()) {
+        if (tombstoned.has(a.id)) continue;
+        const c = a.content || {};
+        if (c.root && tombstoned.has(c.root)) continue;
+        if (a.type === 'vote' && tombstoned.has(c.vote?.link)) continue;
+        if (c.key && tombstoned.has(c.key)) continue;
+        if (c.branch && tombstoned.has(c.branch)) continue;
+        if (c.target && tombstoned.has(c.target)) continue;
+        if (a.type === 'document') {
+          const url = c.url;
+          const ok = await hasBlob(ssbClient, url);
+          if (!ok) continue;
+        }
+        if (a.type === 'forum' && c.root) {
+          const rootId = typeof c.root === 'string' ? c.root : (c.root?.key || c.root?.id || '');
+          const rootAction = idToAction.get(rootId);
+          a.content.rootTitle = rootAction?.content?.title || a.content.rootTitle || '';
+          a.content.rootKey = rootId || a.content.rootKey || '';
+        }
+        latest.push({ ...a, tipId: idToTipId.get(a.id) || a.id });
+      }
 
       let deduped = latest.filter(a => !a.tipId || a.tipId === a.id);
 
       const mediaTypes = new Set(['image','video','audio','document','bookmark']);
       const perAuthorUnique = new Set(['karmaScore']);
       const byKey = new Map();
+      const norm = s => String(s || '').trim().toLowerCase();
+
       for (const a of deduped) {
+        const c = a.content || {};
+        const effTs =
+          (c.updatedAt && Date.parse(c.updatedAt)) ||
+          (c.createdAt && Date.parse(c.createdAt)) ||
+          (a.ts || 0);
+
         if (mediaTypes.has(a.type)) {
-          const u = a.content?.url || a.content?.title || `${a.type}:${a.id}`;
+          const u = c.url || c.title || `${a.type}:${a.id}`;
           const key = `${a.type}:${u}`;
           const prev = byKey.get(key);
-          if (!prev || a.ts > prev.ts) byKey.set(key, a);
+          if (!prev || effTs > prev.__effTs) byKey.set(key, { ...a, __effTs: effTs });
         } else if (perAuthorUnique.has(a.type)) {
           const key = `${a.type}:${a.author}`;
           const prev = byKey.get(key);
-          if (!prev || a.ts > prev.ts) byKey.set(key, a);
+          if (!prev || effTs > prev.__effTs) byKey.set(key, { ...a, __effTs: effTs });
+        } else if (a.type === 'tribe') {
+          const t = norm(c.title);
+          if (t) {
+            const key = `tribe:${t}::${a.author}`;
+            const prev = byKey.get(key);
+            if (!prev || effTs > prev.__effTs) byKey.set(key, { ...a, __effTs: effTs });
+          } else {
+            const key = `id:${a.id}`;
+            byKey.set(key, { ...a, __effTs: effTs });
+          }
         } else {
           const key = `id:${a.id}`;
-          byKey.set(key, a);
+          byKey.set(key, { ...a, __effTs: effTs });
         }
       }
-      deduped = Array.from(byKey.values());
+      deduped = Array.from(byKey.values()).map(x => { delete x.__effTs; return x });
 
       let out;
       if (filter === 'mine') out = deduped.filter(a => a.author === userId);

+ 32 - 4
src/models/stats_model.js

@@ -91,6 +91,26 @@ module.exports = ({ cooler }) => {
     return out;
   };
 
+  const norm = s => String(s || '').normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
+  const bestContentTs = (c, fallbackTs = 0) =>
+    Number(c?.updatedAt ? Date.parse(c.updatedAt) : 0) ||
+    Number(c?.createdAt ? Date.parse(c.createdAt) : 0) ||
+    Number(c?.timestamp || 0) ||
+    Number(fallbackTs || 0);
+  const dedupeTribesNodes = (nodes = []) => {
+    const pick = new Map();
+    for (const n of nodes) {
+      const c = n?.content || {};
+      const title = c.title || c.name || '';
+      const author = n?.author || '';
+      const key = `${norm(title)}::${author}`;
+      const ts = bestContentTs(c, n?.ts || 0);
+      const prev = pick.get(key);
+      if (!prev || ts > prev._ts) pick.set(key, { ...n, _ts: ts });
+    }
+    return Array.from(pick.values());
+  };
+
   const getStats = async (filter = 'ALL') => {
     const ssbClient = await openSsb();
     const userId = ssbClient.id;
@@ -151,12 +171,21 @@ module.exports = ({ cooler }) => {
       }
     }
 
+    const tribeTipNodes = Array.from(tipOf['tribe'].values());
+    const tribeDedupNodes = dedupeTribesNodes(tribeTipNodes);
+    const tribeDedupContents = tribeDedupNodes.map(n => n.content);
+
     const content = {};
     const opinions = {};
     for (const t of types) {
       if (t === 'karmaScore') continue;
-      let vals = Array.from(tipOf[t].values()).map(v => v.content);
-      if (t === 'forum') vals = vals.filter(c => !(c.root && tombTargets.has(c.root)));
+      let vals;
+      if (t === 'tribe') {
+        vals = tribeDedupContents;
+      } else {
+        vals = Array.from(tipOf[t].values()).map(v => v.content);
+        if (t === 'forum') vals = vals.filter(c => !(c.root && tombTargets.has(c.root)));
+      }
       content[t] = vals.length || 0;
       opinions[t] = vals.filter(e => Array.isArray(e.opinions_inhabitants) && e.opinions_inhabitants.length > 0).length || 0;
     }
@@ -179,8 +208,7 @@ module.exports = ({ cooler }) => {
       content['karmaScore'] = sumKarma;
     }
 
-    const tribeVals = Array.from(tipOf['tribe'].values()).map(v => v.content);
-    const memberTribes = tribeVals
+    const memberTribes = tribeDedupContents
       .filter(c => Array.isArray(c.members) && c.members.includes(userId))
       .map(c => c.name || c.title || c.id);