diff --git a/psf-memo-client/src/services/notifications-page.js b/psf-memo-client/src/services/notifications-page.js index a315d52..b9505f4 100644 --- a/psf-memo-client/src/services/notifications-page.js +++ b/psf-memo-client/src/services/notifications-page.js @@ -56,3 +56,7 @@ class NotificationsPage { NotificationsPage.NOTIFICATIONS_PATH = NOTIFICATIONS_PATH module.exports = NotificationsPage + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-02T19:56:07.544Z","module_hash":"647bc4fca66811725670506e32f4db2a94b387b4d318a658a925490607b2caa7","functions":[{"id":"func/NotificationsPage.constructor","name":"NotificationsPage.constructor","line":13,"end_line":19,"hash":"52eca9540d0f0c524cd8b2c6c2a80c37cc1769c2a109bae7c97f080bede56130"},{"id":"func/NotificationsPage.getMyAddress","name":"NotificationsPage.getMyAddress","line":21,"end_line":23,"hash":"3e5d4ac4df379300933a772020528b4ecf4ed83c7386a066f5c270df81adcddd"},{"id":"func/NotificationsPage.load","name":"NotificationsPage.load","line":25,"end_line":45,"hash":"99ce6b9176f964e8794325b59c7e67318514b64388117df930e5addb8ece7ea5"},{"id":"func/NotificationsPage.canLoadMore","name":"NotificationsPage.canLoadMore","line":47,"end_line":49,"hash":"634983bcc6bbe560daad8326db0dd4bf31d5cb9e45c40112565351dceaf8e5d5"},{"id":"func/NotificationsPage.getNotification","name":"NotificationsPage.getNotification","line":51,"end_line":53,"hash":"d01238c7fddb85e9ca7427d512c828ec6524657feb1156a820516641ed27105c"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-db/src/adapters/notifications-query.js b/psf-memo-db/src/adapters/notifications-query.js index 52c7edb..bac117b 100644 --- a/psf-memo-db/src/adapters/notifications-query.js +++ b/psf-memo-db/src/adapters/notifications-query.js @@ -52,6 +52,7 @@ class NotificationsQuery { this._collectFollowNotifications = this._collectFollowNotifications.bind(this) this._collectLikeNotifications = this._collectLikeNotifications.bind(this) this._collectReplyNotifications = this._collectReplyNotifications.bind(this) + this._replyNotificationChild = this._replyNotificationChild.bind(this) this._sortNotifications = this._sortNotifications.bind(this) } @@ -120,11 +121,8 @@ class NotificationsQuery { const childTxid = child?.childTxid if (!parentTxid || !childTxid) continue - const parent = await this._getPostOrNull(parentTxid) - if (!parent || parent.addr !== addr) continue - - const childPost = await this._getPostOrNull(childTxid) - if (!childPost || childPost.addr === addr) continue + const childPost = await this._replyNotificationChild(child, addr) + if (!childPost) continue notifications.push({ type: 'reply', @@ -140,6 +138,21 @@ class NotificationsQuery { return notifications } + // Return the child post when a child record is a valid reply notification + // for addr: it links a parent authored by addr to a child authored by someone + // else. Returns null when the parent is missing or not authored by addr, or + // the child is missing or authored by addr. Callers must have already + // verified the record carries parentTxid and childTxid. + async _replyNotificationChild (child, addr) { + const parent = await this._getPostOrNull(child.parentTxid) + if (!parent || parent.addr !== addr) return null + + const childPost = await this._getPostOrNull(child.childTxid) + if (!childPost || childPost.addr === addr) return null + + return childPost + } + _sortNotifications (notifications) { return notifications.sort((a, b) => { if (b.blockHeight !== a.blockHeight) return b.blockHeight - a.blockHeight @@ -162,3 +175,7 @@ class NotificationsQuery { } export default NotificationsQuery + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-02T20:02:58.120Z","module_hash":"1a298fb4ce5a783162b6e34e8840073f97e0f74c1dc29c675e4d8954ea6c7ae6","functions":[{"id":"func/NotificationsQuery.constructor","name":"NotificationsQuery.constructor","line":12,"end_line":57,"hash":"962099cdc7a50291b0998e09081f53d6d1ee88cdabda016b9318476ce6129507"},{"id":"func/NotificationsQuery._getPostOrNull","name":"NotificationsQuery._getPostOrNull","line":59,"end_line":66,"hash":"5457ca98de35d7ad6569365f4061c56fdb1495db9d2429f267c5c9fee3f2bb00"},{"id":"func/NotificationsQuery._collectFollowNotifications","name":"NotificationsQuery._collectFollowNotifications","line":69,"end_line":90,"hash":"702150793e1b80639ff125864d0841f8037a83701b1773e5e41451cc087d2995"},{"id":"func/NotificationsQuery._collectLikeNotifications","name":"NotificationsQuery._collectLikeNotifications","line":93,"end_line":113,"hash":"1ec11eb62288a1353a74dfd21f8e01635aca72764b01afe033b57c06b713d5d0"},{"id":"func/NotificationsQuery._collectReplyNotifications","name":"NotificationsQuery._collectReplyNotifications","line":116,"end_line":139,"hash":"ab0acd3f37231a52065caee1728913abaa8c9d154d1a7068f743588521a94d4b"},{"id":"func/NotificationsQuery._replyNotificationChild","name":"NotificationsQuery._replyNotificationChild","line":146,"end_line":154,"hash":"b45fbb877dc7fabd01da40362fe5ab92e2729d4f86cf2a27ac129a5f37bb0e5e"},{"id":"func/NotificationsQuery._sortNotifications","name":"NotificationsQuery._sortNotifications","line":156,"end_line":161,"hash":"61b4ae0b6d450e172db3e59240a3c2ccd5fdf0e6ea1e31023df7bb3d52fdbd5c"},{"id":"func/NotificationsQuery.listNotifications","name":"NotificationsQuery.listNotifications","line":164,"end_line":174,"hash":"02d410897747d5e7a7c7ab1df393dfef13e61569ea53442922786ffd7f172554"}]} +// mutate4javascript-manifest-end diff --git a/psf-memo-db/src/use-cases/list-notifications.js b/psf-memo-db/src/use-cases/list-notifications.js index 1cd397d..7bfe372 100644 --- a/psf-memo-db/src/use-cases/list-notifications.js +++ b/psf-memo-db/src/use-cases/list-notifications.js @@ -33,3 +33,7 @@ class ListNotifications extends ListUseCase { } export default ListNotifications + +// mutate4javascript-manifest-begin +// {"version":1,"tested_at":"2026-09-02T19:56:06.917Z","module_hash":"9cee2e299acbb1203a40425d6d429eec8b1e3978f67fb0f3e76f5c32644d956c","functions":[{"id":"func/ListNotifications.constructor","name":"ListNotifications.constructor","line":12,"end_line":14,"hash":"84b86af8804c5aa8b25ec55f1190863b4652d4460d3d25f0cde0fdad2efda18f"},{"id":"func/ListNotifications.execute","name":"ListNotifications.execute","line":16,"end_line":32,"hash":"59384e928f105a7995ff014b410e0832f812a306c4598e19cfe18e9be570d0f0"}]} +// mutate4javascript-manifest-end diff --git a/specifier-prompt.md b/specifier-prompt.md index 24bd49d..cb45f27 100644 --- a/specifier-prompt.md +++ b/specifier-prompt.md @@ -147,7 +147,16 @@ needed. ## 5. Goal & feature backlog The full backlog lives at `specs/feature-backlog.md` and is refreshed below. -Goal: reach feature parity with [memo.cash](https://memo.cash). + +### Current direction (2026-09-03) + +Core functionality is implemented and shipped. All previously listed roadmap +features (P0–P6) have been removed from the backlog. For the foreseeable future +the focus is **front-end improvements** to `psf-memo-client` (the React SPA): +UI/UX polish, accessibility, performance, responsiveness, state handling, error +surfacing, and other client-side improvements. A single user-facing feature may +still touch more than one component; call out all affected components in the +task description and in the handoff. ### Research (2026-08-27) @@ -162,24 +171,6 @@ Goal: reach feature parity with [memo.cash](https://memo.cash). `follow`/`unfollow`, `topicMessage`, `topicFollow`/`topicUnfollow`). The main gaps are client UI and high-level REST read APIs. -### Prioritized roadmap - -| Tier | Features | Status | -|------|----------|--------| -| **P0** | Post, Set name, Reply, Efficient pagination | ✅ shipped | -| **P1** | Like counts (read side) ✅; client display ✅; Set profile text ✅; Set profile picture ✅; Follow/Unfollow ✅ | ✅ shipped | -| **P2** | Topics (list ✅, feed ✅, post/follow/unfollow) | next | -| **P3** | Polls (create, add option, vote) | ✅ shipped (task `poll-actions`) | -| **P4** | Mute / unmute user | later | -| **P5** | Send money memo action, MIP-0009 token exchange | later | -| **P6** | Repost, ranked feed, notifications, search, tags, following feed | later | - -**Suggested next spec:** P2.3/P2.4/P2.5 — add a topic post composer and topic -follow/unfollow buttons on the topic feed page that broadcast `0x6d0c`/ -`0x6d0d`/`0x6d0e`, plus the DB read side (topic follow state, topic followers). -The indexer already stores topic messages and follows in `roomsDb`; the missing -pieces are the client write path and the DB read side for follows. - --- ## 6. Memo protocol reference (action bytes) @@ -368,5 +359,6 @@ At the end of each session, update this file: - Note the current `master` HEAD commit. - State the next feature to work on. -Current `master` HEAD: `4422372` (merge of the architect's second-pass hardening of the Following feed — P6.6 — verified: client build/test/lint and DB test/lint all pass). -Next action: **P6.3 Notifications** — the Gherkin spec is drafted at `psf-memo-client/specs/notifications.feature` (8 scenarios, parsed clean, DRY-checked) and is awaiting user approval before handoff to the coder. After approval, commit it (`By specifier.`), invent the stable task name `notifications`, and send the `git_handoff` to the coder. Send money (P5.1) was skipped by user decision (no clear use case). Remaining candidates after notifications: P5.2–5.5 (MIP-0009 token exchange), P6.1 Repost, P6.2 Ranked feed, P6.5 Tags/hashtags. +Current `master` HEAD: `d3903fa` (backlog cleared of all previously listed +features; direction set to front-end improvements to `psf-memo-client`). +Next action: **ask the user for the next front-end improvement to spec**. diff --git a/specs/feature-backlog.md b/specs/feature-backlog.md index c9002d8..6f66786 100644 --- a/specs/feature-backlog.md +++ b/specs/feature-backlog.md @@ -1,8 +1,8 @@ -# psf-memo — Prioritized Feature Backlog +# psf-memo — Feature Backlog -**Status**: DRAFT — research refresh 2026-08-27. +**Status**: DRAFT — refreshed 2026-09-03. **Owner**: specifier. -**Last updated**: 2026-08-29 +**Last updated**: 2026-09-03 --- @@ -16,6 +16,21 @@ that is broadcast from the client to the chain and later indexed by --- +## Current direction + +Core functionality is implemented and shipped. For the foreseeable future the +focus is **front-end improvements** to `psf-memo-client` (the React SPA). + +- All previously listed roadmap features (P0–P6) have been removed from this + backlog. +- New work should target the client: UI/UX polish, accessibility, performance, + responsiveness, state handling, error surfacing, and any other front-end + improvements. +- A single user-facing feature may still touch more than one component; call + out all affected components in the task description and in the handoff. + +--- + ## Research notes - **Protocol reference**: `https://memo.sv/protocol` (Wayback Machine snapshot @@ -24,19 +39,8 @@ that is broadcast from the client to the chain and later indexed by `memo.cash` implementation. - **memo.cash access**: the live site is behind Cloudflare. Direct `curl` and headless Firefox login attempts from this environment were blocked, so the - roadmap is derived from the protocol spec plus an audit of the existing + roadmap was derived from the protocol spec plus an audit of the existing mono-repo code. -- **Implementation audit** (2026-08-27): - - Indexer already has handlers for: `setName`, `post`, `reply`, `like`, - `setProfile`, `follow`/`unfollow`, `setProfilePic`, `topicMessage`, - `topicFollow`/`topicUnfollow`. - - `psf-memo-db` already has LevelDB stores for all of those actions and raw - `/level/*` CRUD routes. - - Client already supports: posting, replying, setting name, and the like/tip - broadcast UI. - - **Main gaps**: client UI for setting profile text/picture and - follow/unfollow; high-level read APIs for like counts, follow state, topic - feeds, polls, and mutes. --- @@ -100,137 +104,6 @@ Reference: https://memo.sv/protocol (Wayback snapshot 2025-12-15) --- -## Tier P0 — Already shipped to `master` - -| # | Feature | Memo action | Components | Status | -|---|---------|-------------|------------|--------| -| 0.1 | Post a Memo | `0x6d02` | C, I, D | ✅ | -| 0.2 | Set display name | `0x6d01` | C, I, D | ✅ | -| 0.3 | Reply to a Memo | `0x6d03` | C, I, D | ✅ | -| 0.4 | Efficient post pagination | read | D, I | ✅ | - ---- - -## Tier P1 — Core social verbs (close the read loop) - -These features already have indexer storage and (for like/tip) client -broadcast. The remaining work is exposing the read side in `psf-memo-db` and -adding/editing the client UI. - -| # | Feature | Memo action | Components | Status | Next work | -|---|---------|-------------|------------|--------|-----------| -| 1.1 | Like / tip a Memo — read side | `0x6d04` | D | ✅ | `likeCount` returned on `/posts/*` and `/posts/:txid/thread` | -| 1.2 | Like / tip a Memo — client display | `0x6d04` | C | ✅ | Feed/Profile/Thread read `likeCount` from API instead of defaulting to 0 | -| 1.3 | Set profile text (bio) | `0x6d05` | C | ✅ | C: "Set Bio" UI on Account page broadcasts `0x6d05` (217-byte limit) | -| 1.4 | Set profile picture | `0x6d0a` | C, I, D | ✅ | C: "Set Avatar URL" UI broadcasts `0x6d0a` (217-byte limit); I/D already read | -| 1.5 | Follow a user | `0x6d06` | C, I, D | ✅ | C: follow button on profile; D: follow state + following/followers lists | -| 1.6 | Unfollow a user | `0x6d07` | C, I, D | ✅ | C: unfollow button; D: follow state | - -### Priority order within P1 - -1. **Like / tip a Memo — read side** ✅ DONE. -2. **Like / tip a Memo — client display** ✅ DONE. The feed, profile, and - thread views now read `likeCount` from the API (profile shows a read-only - like button). -3. **Set profile text** ✅ DONE. Account page "Set Bio" UI broadcasts `0x6d05` with a 217-byte limit and byte counter (task `set-bio`). -4. **Set profile picture** ✅ DONE. Account page "Set Avatar URL" UI broadcasts `0x6d0a` with a 217-byte limit and byte counter (task `set-avatar-url`). -5. **Follow / Unfollow user** ✅ DONE. Profile page Follow/Unfollow button broadcasts `0x6d06`/`0x6d07`; DB exposes follow state and following/followers lists (task `follow-user`). - -### Like / tip details - -- The like action carries the liked post txid (32 bytes). A pure like has no - BCH output to the author; a tip adds a P2PKH output paying the author. -- `psf-memo-indexer` stores each like in `likesDb` and records `tip` (sats) - when the like tx pays the author. -- `psf-memo-db` aggregates `likesDb` into per-post `likeCount` in - `/posts/recent`, `/posts/by/:addr`, and `/posts/:txid/thread` responses. -- The client already has `MemoLike`, `LikeTipPage`, `LikeButton`, and - `LikeTipModal`; the feed/profile/thread views now read `likeCount` from the - API (profile renders a read-only `LikeButton`). - ---- - -## Tier P2 — Topics - -Topics add a second feed axis. The indexer already stores topic messages and -follows in `roomsDb`; the DB and client need query/render support. - -| # | Feature | Memo action | Components | Status | -|---|---------|-------------|------------|--------| -| 2.1 | Topic list / discovery | read | D, C | ✅ | -| 2.2 | Topic feed page | read | D, C | ✅ | -| 2.3 | Post a topic message | `0x6d0c` | C, I, D | ✅ shipped (task `topic-actions`) | -| 2.4 | Follow a topic | `0x6d0d` | C, I, D | ✅ shipped (task `topic-actions`) | -| 2.5 | Unfollow a topic | `0x6d0e` | C, I, D | ✅ shipped (task `topic-actions`) | - ---- - -## Tier P3 — Polls - -Polls require a new data model and rendering. The indexer has no handler yet. - -| # | Feature | Memo action | Components | Status | -|---|---------|-------------|------------|--------| -| 3.1 | Create a poll | `0x6d10` | C, I, D | ✅ shipped (task `poll-actions`) | -| 3.2 | Add a poll option | `0x6d13` | C, I, D | ✅ shipped (task `poll-actions`) | -| 3.3 | Vote in a poll | `0x6d14` | C, I, D | ✅ shipped (task `poll-actions`) | - ---- - -## Tier P4 — Moderation - -| # | Feature | Memo action | Components | Status | -|---|---------|-------------|------------|--------| -| 4.1 | Mute a user | `0x6d16` | C, D, I | ✅ shipped (task `mute-user`) | -| 4.2 | Unmute a user | `0x6d17` | C, D, I | ✅ shipped (task `mute-user`) | - ---- - -## Tier P5 — Money & token exchange - -| # | Feature | Memo action | Components | Status | -|---|---------|-------------|------------|--------| -| 5.1 | Send money with memo | `0x6d24` | C | 🔴 missing | -| 5.2 | Token sell offer | `0x6d30` (MIP-0009) | C, I, D | 🔴 missing | -| 5.3 | Token buy offer | `0x6d31` (MIP-0009) | C, I, D | 🔴 missing | -| 5.4 | Attach token sale signature | `0x6d32` (MIP-0009) | C, I, D | 🔴 missing | -| 5.5 | Pin token post | `0x6d35` (MIP-0009) | C, I, D | 🔴 missing | - ---- - -## Tier P6 — Advanced social & discovery (later) - -| # | Feature | Components | Notes | -|---|---------|------------|-------| -| 6.1 | Repost a memo | C, I, D | `0x6d0b` is marked *planned* in the protocol | -| 6.2 | Ranked feed | C, D | memo.cash "ranked" post ordering | -| 6.3 | Notifications | C, D | replies / likes / follows to my posts | -| 6.4 | Search | C, D | posts / profiles / topics — ✅ shipped (task `search`) | -| 6.5 | Tags / hashtags | C, D | link + filter by tag | -| 6.6 | Following feed | C, D | feed filtered to followed users — ✅ shipped (task `following-feed`) | - ---- - -## Suggested next spec - -**Following feed (P6.6)** — ✅ shipped (task `following-feed`), merged from the pipeline and verified (client build/test/lint, DB test/lint). A second-pass hardening merge from the architect (2026-09-02) extracted `parseRequiredString` into `pagination.js` (shared by the addr/room-scoped list use cases) and added client + DB composition-root hardening tests; merged to `master` at `4422372` and re-verified. Following Feed is a read-only aggregation: the viewer sees top-level posts (replies excluded) authored only by profiles they follow, newest first, never their own posts. -- `psf-memo-db`: `GET /posts/following/:addr?limit=&offset=` returning `{ posts, pagination }`, joining the follows index (`FollowQuery.listFollowing`) with the posts index (`PostQuery.scanFollowingFeedTxidsAndCount`). Empty following or no posts → empty result set. -- `psf-memo-client`: a "Following" nav page at `/posts/following` that reads the viewer's wallet address, calls `MemoDb.getFollowingFeed`, and renders posts like the recent feed; shows "You are not following anyone." when following nobody and "No posts from profiles you follow." when followed profiles have no posts. -- Spec: `psf-memo-client/specs/following-feed.feature`. - -**Search (P6.4)** — ✅ shipped (task `search`), merged from the pipeline and verified (client build/test/lint, DB test/lint): -- `psf-memo-db`: `GET /search?q=&limit=&offset=` returning `{ posts, profiles, pagination }`. A `SearchQuery` adapter scans `postsDb` (top-level posts only, replies excluded via `postParentsDb`), `namesDb` (profile name), and `profilesDb` (profile bio) with case-insensitive substring matching; a `SearchAll` use case; a `/search` router + controller. -- `psf-memo-client`: a `/search` page with a search box; `MemoDb.search(q)`; a `SearchPage` service; render matching posts like the feed and matching profiles like recent-profiles. -- Empty queries and no-match queries both return an empty result set (no error). -- Spec: `psf-memo-client/specs/search.feature`. - -### Deferred / skipped - -- **Send money with memo (P5.1)** — `0x6d24` — skipped by user decision - (no clear use case yet). - ---- - ## Notes for future cycles - Broadcast result (txid) is returned immediately; the action appears in the @@ -239,11 +112,3 @@ Polls require a new data model and rendering. The indexer has no handler yet. - Mutations/specs are Gherkin feature files under per-component `specs/` in the format defined by github.com/unclebob/Acceptance-Pipeline-Specification. - Root `specs/` contains this backlog and cross-component architecture notes. - -## Completed fixes (not roadmap features) - -- **ZMQ-mode DB backups** (2026-08-28, task `zmq-db-backups`): the block - indexer only created zip backups during IBD; the ZMQ live loop never called - `backupDb()`. Fixed by centralizing the `height % epoch === 0` decision in a - `BackupDb.maybeBackupDb` use case called from both the IBD and ZMQ paths. - Spec: `psf-memo-indexer/specs/zmq-mode-db-backups.feature`.