mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
-21
@@ -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**.
|
||||
|
||||
+19
-154
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user