From fc9539a618994e3c67dfe35681f4111d8ce7f895 Mon Sep 17 00:00:00 2001 From: trav Date: Sat, 22 Aug 2026 20:30:46 -0400 Subject: [PATCH] /items shows items only, not custody transfers A `nft: "give"` message is a change of custody, not a thing. It has no photo of its own, so it borrowed the photo of the item it transferred - which meant the same object appeared on the grid twice, a few cards apart, distinguished only by a dimmed image and an id overlay. That reads as a duplicate, not as a hand-off. Custody is better answered one level down. Each card already links to its item's thread, where the mint and every hand-off since are in order, with the steward resolved. So the overlay was showing a truncated feed id on the grid to save a click that is worth making. Filters on nft === "mint" rather than excluding gives: every custodisco message carries one of the two - checked across all 407 - so matching mint is exact. 301 items from 3 feeds, down from 407 entries. The feed count now counts feeds that actually contributed an item rather than the size of the follow set. The pub follows itself and publishes no items, so the old number was one too high and would have drifted further as feeds get followed for other reasons. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0192zBTNZKZn5svyJ5HTnYds --- index.js | 2 -- items.js | 69 ++++++++++++++++++++++--------------------------------- render.js | 42 ++++++++------------------------- 3 files changed, 37 insertions(+), 76 deletions(-) diff --git a/index.js b/index.js index d802e61..a1a9c11 100644 --- a/index.js +++ b/index.js @@ -118,8 +118,6 @@ exports.init = function (sbot, config) { var serveItems = createItemsHandler({ sbot: sbot, defaultOpts: defaultOpts, - getMsg: getMsg, - getAbout: getAbout, addAuthorAbout: addAuthorAbout, renderItemGrid: renderItemGrid, wrapPage: wrapPage, diff --git a/items.js b/items.js index 0344eea..c0c00fb 100644 --- a/items.js +++ b/items.js @@ -9,6 +9,12 @@ var getFollows = require('./lib/follows') // kiosk feed ids: adding a kiosk means following it from the pub, and its items // appear here on the next cache miss. No code change, no edit to cust.ooo. // +// ITEMS ONLY. A `nft: "give"` message is a change of custody, not a thing - it +// has no photo of its own and describes an item already on the page, so showing +// it here listed the same object twice. Custody is answered one level down: the +// card links to the item's thread, where the mint and every hand-off since are +// in order. +// // Two facts about the data that this file depends on, both verified against the // live log and both easy to get wrong: // * `custodisco` is the STRING "true", not a boolean. @@ -45,14 +51,17 @@ function itemCaption (c) { .trim() } -function isCustodisco (msg) { +// Every custodisco message carries nft: "mint" or "give" - checked across all +// 407 of them, none lacks the field - so matching "mint" is exact rather than +// merely excluding gives. +function isItem (msg) { var c = msg && msg.value && msg.value.content - return !!c && typeof c === 'object' && c.custodisco === 'true' + return !!c && typeof c === 'object' && + c.custodisco === 'true' && c.nft === 'mint' } // ctx supplies the pieces that live in index.js's init closure: -// sbot, defaultOpts, getMsg, getAbout, addAuthorAbout, respond, toPull, -// renderItemGrid, wrapPage +// sbot, defaultOpts, addAuthorAbout, respond, toPull, renderItemGrid, wrapPage module.exports = function createItemsHandler (ctx) { var cache = null // { at, msgs, feeds } @@ -73,7 +82,7 @@ module.exports = function createItemsHandler (ctx) { pull( ctx.sbot.createLogStream({ reverse: true }), pull.filter(function (msg) { - return isCustodisco(msg) && authors[msg.value.author] === true + return isItem(msg) && authors[msg.value.author] === true }), pull.collect(function (err, msgs) { if (err) return cb(err) @@ -81,10 +90,17 @@ module.exports = function createItemsHandler (ctx) { // whenever an old feed is backfilled. Sort so "newest first" and // the ?before cursor both mean the same thing. msgs.sort(function (a, b) { return b.value.timestamp - a.value.timestamp }) + // Count the feeds that actually contributed an item, not the size + // of the follow set: the pub follows itself and publishes no items, + // so the follow set overstates it by one and would keep drifting + // as feeds are followed for other reasons. + var contributing = Object.create(null) + msgs.forEach(function (m) { contributing[m.value.author] = true }) + cache = { at: Date.now(), msgs: msgs, - feeds: Object.keys(authors).length + feeds: Object.keys(contributing).length } cb(null, cache) }) @@ -93,40 +109,11 @@ module.exports = function createItemsHandler (ctx) { }) } - // A transfer carries no photo of its own, so borrow the parent item's and - // resolve the new steward's display name. Both lookups go through the LRU - // memos index.js already keeps. - function addTransferContext (msg, cb) { + function addItemContext (msg, cb) { var c = msg.value.content - if (c.nft !== 'give') { - msg.itemPhoto = itemPhoto(c) - msg.itemCaption = itemCaption(c) - return cb(null, msg) - } - - var pending = 2 - function done () { - if (--pending) return - cb(null, msg) - } - - if (c.root) { - ctx.getMsg(c.root, function (err, root) { - var rc = root && root.value && root.value.content - if (rc) { - msg.itemPhoto = itemPhoto(rc) - msg.itemCaption = itemCaption(rc) - } - done() - }) - } else done() - - if (c.target) { - ctx.getAbout(c.target, function (err, about) { - if (about) msg.stewardAbout = about - done() - }) - } else done() + msg.itemPhoto = itemPhoto(c) + msg.itemCaption = itemCaption(c) + cb(null, msg) } return function serveItems (req, res, query) { @@ -151,7 +138,7 @@ module.exports = function createItemsHandler (ctx) { pull( pull.values(page), paramap(ctx.addAuthorAbout, 8), - paramap(addTransferContext, 8), + paramap(addItemContext, 8), pull( ctx.renderItemGrid(ctx.defaultOpts, { total: data.msgs.length, @@ -172,4 +159,4 @@ module.exports = function createItemsHandler (ctx) { module.exports.itemPhoto = itemPhoto module.exports.itemCaption = itemCaption -module.exports.isCustodisco = isCustodisco +module.exports.isItem = isItem diff --git a/render.js b/render.js index 1bf4a3e..d6e7ea1 100644 --- a/render.js +++ b/render.js @@ -392,18 +392,6 @@ var styles = ` text-align: center; } .item-photo-missing::after { content: "photo not fetched yet"; } - .item-card-give .item-photo { opacity: 0.55; } - .item-badge { - position: absolute; - left: 0; right: 0; bottom: 0; - padding: 6px 8px; - background-color: rgba(33, 37, 41, 0.82); - color: #f1f3f5; - font-size: 13px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } .item-meta { padding: 10px 12px; display: flex; flex-direction: column; gap: 3px; } .item-caption { line-height: 1.35em; @@ -721,8 +709,9 @@ function renderShowAll(showAll, url) { // The custo item grid (/items) // // Cards, not articles: this is a collection of things, and a single-column feed -// of 300 photos reads like a mailing list rather than an archive. A transfer -// borrows the photo of the item it transfers, so the grid stays regular. +// of 300 photos reads like a mailing list rather than an archive. Items only - +// who holds an item is answered by its thread, one click in, not by an overlay +// on the grid. // --------------------------------------------------------------------------- function renderItemGrid(opts, meta) { @@ -734,7 +723,7 @@ function renderItemGrid(opts, meta) { } function itemsHeader(opts, meta) { - var counted = meta.total === 1 ? '1 item' : meta.total + ' items and transfers' + var counted = meta.total === 1 ? '1 item' : meta.total + ' items' var feeds = meta.feeds === 1 ? '1 feed' : meta.feeds + ' feeds' return h('header.items-header', h('h1', 'custo items'), @@ -758,11 +747,6 @@ function itemsFooter(opts, meta) { function renderItemCard(opts, msg) { var c = msg.value.content - var isGive = c.nft === 'give' - - // A transfer links to the item it transfers, not to itself: the thread page - // shows the mint and every hand-off since, which is the interesting view. - var target = isGive && c.root ? c.root : msg.key var media if (msg.itemPhoto) { @@ -779,15 +763,12 @@ function renderItemCard(opts, msg) { media = h('div.item-photo.item-photo-missing') } - var badge = isGive - ? h('span.item-badge', '\u2192 ' + stewardName(msg, c)) - : '' + var caption = msg.itemCaption || itemFallbackCaption(c) - var caption = msg.itemCaption || (isGive ? 'an item' : itemFallbackCaption(c)) - - return h('a.item-card' + (isGive ? '.item-card-give' : ''), - { href: opts.base + encodeURIComponent(target) }, - h('div.item-media', media, badge), + // Links to the item's own thread: the mint, and every hand-off since. + return h('a.item-card', + { href: opts.base + encodeURIComponent(msg.key) }, + h('div.item-media', media), h('div.item-meta', h('span.item-caption', caption), h('span.item-by', msg.author && msg.author.name), @@ -795,11 +776,6 @@ function renderItemCard(opts, msg) { ).outerHTML } -function stewardName(msg, c) { - if (msg.stewardAbout && msg.stewardAbout.name) return msg.stewardAbout.name - return String(c.target || 'someone').substr(0, 10) + '\u2026' -} - function itemFallbackCaption(c) { return String(c.text || '').replace(/\s+/g, ' ').trim().substr(0, 140) || 'untitled' }