psy пре 2 дана
родитељ
комит
c217955a5d
5 измењених фајлова са 67 додато и 105 уклоњено
  1. 6 0
      docs/CHANGELOG.md
  2. 42 85
      src/models/tribes_model.js
  3. 1 1
      src/server/package-lock.json
  4. 1 1
      src/server/package.json
  5. 17 18
      src/views/tribes_view.js

+ 6 - 0
docs/CHANGELOG.md

@@ -13,6 +13,12 @@ All notable changes to this project will be documented in this file.
 ### Security
 -->
 
+## v0.5.2 - 2025-10-27
+
+### Fixed
+
+ + Tribes duplication (Tribes plugin).
+
 ## v0.5.2 - 2025-10-22
 
 ### Added

+ 42 - 85
src/models/tribes_model.js

@@ -43,7 +43,6 @@ module.exports = ({ cooler }) => {
     async generateInvite(tribeId) {
       const ssb = await openSsb();
       const userId = ssb.id;
-
       const tribe = await this.getTribeById(tribeId);
       if (tribe.inviteMode === 'strict' && tribe.author !== userId) {
         throw new Error('Only the author can generate invites in strict mode');
@@ -51,10 +50,8 @@ module.exports = ({ cooler }) => {
       if (tribe.inviteMode === 'open' && !tribe.members.includes(userId)) {
         throw new Error('Only tribe members can generate invites in open mode');
       }
-
       const code = Math.random().toString(36).substring(2, 10);
       const invites = Array.isArray(tribe.invites) ? [...tribe.invites, code] : [code];
-
       await this.updateTribeInvites(tribeId, invites);
       return code;
     },
@@ -94,7 +91,7 @@ module.exports = ({ cooler }) => {
       members.splice(idx, 1);
       const updatedTribe = {
         type: 'tribe',
-        replaces: tribeId, 
+        replaces: tribeId,
         title: tribe.title,
         description: tribe.description,
         image: tribe.image,
@@ -132,7 +129,7 @@ module.exports = ({ cooler }) => {
       const updatedInvites = tribe.invites.filter(c => c !== code);
       const updatedTribe = {
         type: 'tribe',
-        replaces: tribe.id, 
+        replaces: tribe.id,
         title: tribe.title,
         description: tribe.description,
         image: tribe.image,
@@ -159,7 +156,6 @@ module.exports = ({ cooler }) => {
     async updateTribeMembers(tribeId, members) {
       const ssb = await openSsb();
       const tribe = await this.getTribeById(tribeId);
-
       const updatedTribe = {
         type: 'tribe',
         replaces: tribeId,
@@ -184,7 +180,6 @@ module.exports = ({ cooler }) => {
     async updateTribeFeed(tribeId, newFeed) {
       const ssb = await openSsb();
       const tribe = await this.getTribeById(tribeId);
-
       const updatedTribe = {
         type: 'tribe',
         replaces: tribeId,
@@ -208,10 +203,9 @@ module.exports = ({ cooler }) => {
 
     async publishUpdatedTribe(tribeId, updatedTribe) {
       const ssb = await openSsb();
-      const userId = ssb.id;
-      await this.publishTombstone(tribeId);
       const updatedTribeData = {
         type: 'tribe',
+        replaces: updatedTribe.replaces || tribeId,
         title: updatedTribe.title,
         description: updatedTribe.description,
         image: updatedTribe.image,
@@ -277,62 +271,46 @@ module.exports = ({ cooler }) => {
       ));
     },
 
-    async listAll() {
+     async listAll() {
       const ssb = await openSsb();
       return new Promise((res, rej) => pull(
         ssb.createLogStream({ limit: logLimit }),
         pull.collect((err, msgs) => {
           if (err) return rej(err);
-          const tombstoned = new Set();
-          const replaces = new Map();
-          const tribes = new Map();
-          for (const msg of msgs) {
-            const k = msg.key;
-            const c = msg.value?.content;
-            if (!c) continue;
-            if (c.type === 'tombstone' && c.target) tombstoned.add(c.target);
-            if (c.type === 'tribe') {
-              if (tombstoned.has(k)) continue;
-              if (c.replaces) replaces.set(c.replaces, k);
-              tribes.set(k, { id: k, content: c });
-            }
-          }
-          const seen = new Set();
-          const finalTribes = [];
-
-          for (const [originalId, { content }] of tribes.entries()) {
-            let latestId = originalId;
-            let latestVersion = content;
-            while (replaces.has(latestId)) {
-              latestId = replaces.get(latestId);
-              const nextTribe = tribes.get(latestId);
-              if (nextTribe && nextTribe.content.updatedAt > latestVersion.updatedAt) {
-                latestVersion = nextTribe.content;
-              }
-            }
-            if (!seen.has(latestId) && !tombstoned.has(latestId)) {
-              seen.add(latestId);
-              finalTribes.push({
-                id: latestId,
-                type: latestVersion.type,
-                title: latestVersion.title,
-                description: latestVersion.description,
-                image: latestVersion.image || null,
-                location: latestVersion.location,
-                tags: Array.isArray(latestVersion.tags) ? latestVersion.tags : [],
-                isLARP: latestVersion.isLARP || false,
-                isAnonymous: latestVersion.isAnonymous === false ? false : true,
-                members: Array.isArray(latestVersion.members) ? latestVersion.members : [],
-                invites: Array.isArray(latestVersion.invites) ? latestVersion.invites : [],
-                inviteMode: latestVersion.inviteMode || 'strict',
-                createdAt: latestVersion.createdAt,
-                updatedAt: latestVersion.updatedAt,
-                author: latestVersion.author,
-                feed: Array.isArray(latestVersion.feed) ? latestVersion.feed : []
-              });
-            }
+          const norm = s => (s || '').toString().trim().toLowerCase();
+          const pickNewest = (a, b) => {
+            const ta = Date.parse(a.updatedAt || a.createdAt) || a._ts || 0;
+            const tb = Date.parse(b.updatedAt || b.createdAt) || b._ts || 0;
+            return tb > ta ? b : a;
+          };
+          const byKey = new Map();
+          for (const m of msgs) {
+            const c = m.value?.content;
+            if (!c || c.type !== 'tribe') continue;
+            const item = {
+              id: m.key,
+              type: c.type,
+              title: c.title,
+              description: c.description,
+              image: c.image || null,
+              location: c.location,
+              tags: Array.isArray(c.tags) ? c.tags : [],
+              isLARP: !!c.isLARP,
+              isAnonymous: c.isAnonymous !== false,
+              members: Array.isArray(c.members) ? c.members : [],
+              invites: Array.isArray(c.invites) ? c.invites : [],
+              inviteMode: c.inviteMode || 'strict',
+              createdAt: c.createdAt,
+              updatedAt: c.updatedAt,
+              author: c.author,
+              feed: Array.isArray(c.feed) ? c.feed : [],
+              _ts: m.value?.timestamp
+            };
+            const key = `${norm(item.title)}::${norm(item.author)}`;
+            if (!byKey.has(key)) byKey.set(key, item);
+            else byKey.set(key, pickNewest(byKey.get(key), item));
           }
-          res(finalTribes);
+          res(Array.from(byKey.values()));
         })
       ));
     },
@@ -345,6 +323,7 @@ module.exports = ({ cooler }) => {
         type: 'tribe',
         ...tribe,
         ...updatedContent,
+        replaces: tribeId,
         updatedAt: new Date().toISOString()
       };
       return this.publishUpdatedTribe(tribeId, updatedTribe);
@@ -353,41 +332,19 @@ module.exports = ({ cooler }) => {
     async publishTombstone(tribeId) {
       const ssb = await openSsb();
       const userId = ssb.id;
-
       const tombstone = {
         type: 'tombstone',
         target: tribeId,
         deletedAt: new Date().toISOString(),
         author: userId
       };
-
       await new Promise((resolve, reject) => {
-      ssb.publish(tombstone, (err) => {
-        if (err) return reject(err);
-        resolve();
+        ssb.publish(tombstone, (err) => {
+          if (err) return reject(err);
+          resolve();
         });
       });
-      const tribe = await this.getTribeById(tribeId); 
-      const tombstonedTribe = {
-        type: 'tombstone',
-        title: tribe.title,
-        description: tribe.description,
-        image: tribe.image,
-        location: tribe.location,
-        tags: tribe.tags,
-        isLARP: tribe.isLARP,
-        isAnonymous: tribe.isAnonymous,
-        members: [],
-        invites: [],
-        inviteMode: tribe.inviteMode,
-        createdAt: tribe.createdAt,
-        updatedAt: new Date().toISOString(),
-        author: tribe.author,
-        feed: tribe.feed
-      };
-      return new Promise((resolve, reject) => {
-        ssb.publish(tombstonedTribe, (err, result) => err ? reject(err) : resolve(result));
-      });
+      return;
     },
 
     async refeed(tribeId, messageId) {

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

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

+ 1 - 1
src/server/package.json

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

+ 17 - 18
src/views/tribes_view.js

@@ -12,7 +12,6 @@ const paginateFeedTribesView = (feed, page = 1, itemsPerPage = 5) => {
 
 const renderPaginationTribesView = (page, totalPages, filter) => {
   if (totalPages <= 1) return '';
-
   return div({ class: 'pagination' },
     page > 1 ? form({ method: 'GET', action: '/tribes' },
       input({ type: 'hidden', name: 'filter', value: filter }),
@@ -31,7 +30,6 @@ const renderFeedTribesView = (tribe, page, query, filter) => {
   const feed = Array.isArray(tribe.feed) ? tribe.feed : [];
   const feedFilter = (query.feedFilter || 'TOP').toUpperCase();
   let filteredFeed = feed;
-
   if (feedFilter === 'MINE') filteredFeed = feed.filter(m => m.author === userId);
   if (feedFilter === 'RECENT') {
     const last24h = Date.now() - 86400000;
@@ -46,12 +44,14 @@ const renderFeedTribesView = (tribe, page, query, filter) => {
         return dateB - dateA;
       });
   }
-  if (feedFilter === 'ALL') filteredFeed = [...feed].sort((a, b) => b.date - a.date);
+  if (feedFilter === 'ALL') filteredFeed = [...feed].sort((a, b) => {
+    const da = typeof a.date === "string" ? Date.parse(a.date) : a.date;
+    const db = typeof b.date === "string" ? Date.parse(b.date) : b.date;
+    return db - da;
+  });
   if (feedFilter === 'TOP') filteredFeed = [...feed].sort((a, b) => (b.refeeds || 0) - (a.refeeds || 0));
-
   const totalPages = Math.ceil(filteredFeed.length / 5);
   const paginatedFeed = paginateFeedTribesView(filteredFeed, page);
-
   return div({ class: 'tribe-feed' },
     div({ class: 'feed-actions', style: 'margin-bottom:8px;' },
       ['TOP', 'MINE', 'ALL', 'RECENT'].map(f =>
@@ -155,7 +155,11 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}) => {
 
   const sorted = filter === 'top'
     ? [...searched].sort((a, b) => b.members.length - a.members.length)
-    : [...searched].sort((a, b) => b.createdAt - a.createdAt);
+    : [...searched].sort((a, b) => {
+        const ca = typeof a.createdAt === 'string' ? Date.parse(a.createdAt) : a.createdAt;
+        const cb = typeof b.createdAt === 'string' ? Date.parse(b.createdAt) : b.createdAt;
+        return cb - ca;
+      });
 
   const title =
     filter === 'recent' ? i18n.tribeRecentSectionTitle :
@@ -236,15 +240,6 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}) => {
       option({ value: 'open', selected: tribeToEdit.inviteMode === 'open' ? 'selected' : undefined }, i18n.tribeOpen)
     ),
     br(), br(),   
-    
-    // label({ for: 'isLARP' }, i18n.tribeIsLARPLabel),
-    // br,
-    // select({ name: 'isLARP', id: 'isLARP' },
-    //   option({ value: 'true', selected: tribeToEdit.isLARP === true ? 'selected' : undefined }, i18n.tribeYes),
-    //   option({ value: 'false', selected: tribeToEdit.isLARP === false ? 'selected' : undefined }, i18n.tribeNo)
-    // ),
-    // br(), br(),
-    
     button({ type: 'submit' }, isEdit ? i18n.tribeUpdateButton : i18n.tribeCreateButton)
     )
   ) : null;
@@ -313,7 +308,6 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}) => {
   );
 };
 
-
 const renderFeedTribeView = async (tribe, query = {}, filter) => {
   const feed = Array.isArray(tribe.feed) ? tribe.feed : [];
   const feedFilter = (query.feedFilter || 'RECENT').toUpperCase();
@@ -335,7 +329,11 @@ const renderFeedTribeView = async (tribe, query = {}, filter) => {
       });
   }
   if (feedFilter === 'ALL') {
-    filteredFeed = [...feed].sort((a, b) => b.date - a.date);
+    filteredFeed = [...feed].sort((a, b) => {
+      const da = typeof a.date === 'string' ? Date.parse(a.date) : a.date;
+      const db = typeof b.date === 'string' ? Date.parse(b.date) : b.date;
+      return db - da;
+    });
   }
   if (feedFilter === 'TOP') {
     filteredFeed = [...feed].sort((a, b) => (b.refeeds || 0) - (a.refeeds || 0));
@@ -372,7 +370,7 @@ const renderFeedTribeView = async (tribe, query = {}, filter) => {
   );
 };
 
-exports.tribeView = async (tribe, userId, query) => {
+exports.tribeView = async (tribe, userIdParam, query) => {
   if (!tribe) {
     return div({ class: 'error' }, 'Tribe not found!');
   }
@@ -430,3 +428,4 @@ exports.tribeView = async (tribe, userId, query) => {
     tribeDetails
   );
 };
+