193 lines
7.9 KiB
Markdown
193 lines
7.9 KiB
Markdown
# App Stacks — Forward Plan
|
|
|
|
## Current State
|
|
|
|
[`www/app-stacks.html`](../www/app-stacks.html) has:
|
|
- A publish form that takes a Gitea URL + category and publishes a kind 30078 app-definition event
|
|
- A subscription that displays received app-definition events grouped by category
|
|
|
|
## New Understanding
|
|
|
|
- **Categories are actually app stacks** — kind 30267 events (App curation set)
|
|
- An **app stack** is a curated collection of apps, e.g. "Nostr Clients", "Bitcoin & Lightning Wallets"
|
|
- An **app definition** (kind 30078) belongs to an app stack, not a free-text category
|
|
- We need two separate publish flows:
|
|
1. **Create an App Stack** — publishes a kind 30267 event
|
|
2. **Publish App Definition** — selects an existing app stack to belong to
|
|
|
|
## Nostr Event Types
|
|
|
|
### App Stack (kind 30267)
|
|
```json
|
|
{
|
|
"kind": 30267,
|
|
"content": "{\"name\":\"Nostr Clients\",\"description\":\"Nostr-native social and communication clients.\"}",
|
|
"tags": [
|
|
["d", "nostr-clients"],
|
|
["t", "app-stack"],
|
|
["name", "Nostr Clients"],
|
|
["description", "Nostr-native social and communication clients."],
|
|
// References to apps in this stack:
|
|
["a", "30078:<pubkey>:app-com.vitorpamplona.amethyst", "wss://relay.zapstore.dev"],
|
|
["a", "30078:<pubkey>:app-com.example.other", "wss://relay.zapstore.dev"]
|
|
]
|
|
}
|
|
```
|
|
|
|
### App Definition (kind 30078) — updated
|
|
```json
|
|
{
|
|
"kind": 30078,
|
|
"content": "{\"name\":\"Amethyst\",\"identifier\":\"com.vitorpamplona.amethyst\",\"repository\":\"...\",\"description\":\"\"}",
|
|
"tags": [
|
|
["d", "app-com.vitorpamplona.amethyst"],
|
|
["t", "app-definition"],
|
|
["t", "nostr-clients"], // ← references the app stack's d tag
|
|
["name", "Amethyst"],
|
|
["repository", "https://github.com/vitorpamplona/amethyst"],
|
|
["gitea", "com.vitorpamplona.amethyst"]
|
|
]
|
|
}
|
|
```
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase 1: App Stack CRUD
|
|
|
|
**Goal:** Create, list, and display app stacks (kind 30267 events).
|
|
|
|
**Changes to [`www/app-stacks.html`](../www/app-stacks.html) (JS only):**
|
|
|
|
1. **New subscription** — subscribe to kind 30267 with `#t: app-stack`:
|
|
```javascript
|
|
subscribe({ kinds: [30267], '#t': ['app-stack'], limit: 200 }, ...);
|
|
```
|
|
|
|
2. **New function: `parseAppStack(evt)`** — parse kind 30267 events into `{ dTag, name, description, appRefs: [], eventId }`
|
|
|
|
3. **New function: `renderStacks()`** — display app stacks in a section above or alongside app definitions
|
|
|
|
4. **New function: `publishAppStack(name, description)`** — create and publish a kind 30267 event:
|
|
```javascript
|
|
{
|
|
kind: 30267,
|
|
content: JSON.stringify({ name, description }),
|
|
tags: [
|
|
['d', name.toLowerCase().replace(/\s+/g, '-')],
|
|
['t', 'app-stack'],
|
|
['name', name],
|
|
['description', description],
|
|
],
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
}
|
|
```
|
|
|
|
5. **New function: `showCreateStackForm()`** — form with name and description fields
|
|
|
|
6. **Update `renderApps()`** — add a "Create App Stack" button/area
|
|
|
|
### Phase 2: Link App Definitions to Stacks
|
|
|
|
**Goal:** App definitions select an app stack instead of typing a free-text category.
|
|
|
|
**Changes to [`www/app-stacks.html`](../www/app-stacks.html) (JS only):**
|
|
|
|
1. **Update publish form** — replace the free-text category input with a `<select>` dropdown populated from loaded app stacks
|
|
|
|
2. **Update `doPublish()`** — use the selected stack's `d` tag value as the `#t` category tag
|
|
|
|
3. **Update `renderApps()`** — group apps by the app stack they reference, not by free-text category
|
|
|
|
### Phase 3: Stack Membership Management
|
|
|
|
**Goal:** Add/remove apps from stacks, and display stack membership.
|
|
|
|
**Changes to [`www/app-stacks.html`](../www/app-stacks.html) (JS only):**
|
|
|
|
1. **Update `publishAppDefinition()`** — also add an `a` tag referencing the app stack:
|
|
```javascript
|
|
['a', '30267:<pubkey>:' + stackDTag, 'wss://relay.zapstore.dev']
|
|
```
|
|
|
|
2. **Display stack membership** — show which stack(s) each app belongs to
|
|
|
|
3. **Stack detail view** — show all apps in a stack
|
|
|
|
## Data Flow
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
subgraph "Publishing"
|
|
A[Create App Stack form] --> B[publishEvent kind 30267]
|
|
C[Publish App Definition form] --> D[Select stack from dropdown]
|
|
D --> E[publishEvent kind 30078<br>with #t: stack-d-tag]
|
|
end
|
|
|
|
subgraph "Subscriptions"
|
|
F[Subscribe kind 30267 #t: app-stack] --> G[Collect stacks]
|
|
H[Subscribe kind 30078 #t: app-definition] --> I[Collect apps]
|
|
end
|
|
|
|
subgraph "Rendering"
|
|
G --> J[Render stack list]
|
|
I --> K[Group apps by stack reference]
|
|
K --> L[Render apps under each stack]
|
|
end
|
|
```
|
|
|
|
## Files to Modify
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| [`www/app-stacks.html`](../www/app-stacks.html) | Add JS for kind 30267 subscription, parsing, rendering, publishing. Update app-definition form to use stack dropdown. |
|
|
| [`app-stacks/README.md`](README.md) | Update with new event structures and flow |
|
|
|
|
## What NOT to Change
|
|
|
|
- No HTML changes to the template shell
|
|
- No CSS additions
|
|
- No removal of existing functionality
|
|
|
|
## Corrections Discovered During Implementation
|
|
|
|
### 1. Stack `a` Tags Must Use Kind 32267 (Not 30078)
|
|
|
|
The relay `wss://relay.zapstore.dev` **validates** that `a` tags in kind 30267 events reference kind **32267** (Software Application). Using kind 30078 results in `invalid app identifier: expected kind 32267, got 30078`.
|
|
|
|
The `a` tags in stacks reference the **original app listing events** (kind 32267) published by app developers, not the app-definition events (kind 30078) that the frontend loads. The frontend independently subscribes to kind 30078 `#t: app-definition` and groups them by the `#t` category tag matching the stack's `d` tag — it does not follow the `a` tags.
|
|
|
|
**Current state in** [`app-stacks/publish_all.py`](../app-stacks/publish_all.py:431):
|
|
```python
|
|
["a", f"32267:{pubkey}:{app_id}", "wss://relay.zapstore.dev"]
|
|
```
|
|
|
|
The pubkey is still a placeholder (the signer's own pubkey) since we don't have the actual per-app pubkeys from the original kind 32267 events. This is acceptable because the frontend doesn't use the `a` tags for display — it uses the `#t` category tag on the app-definition events.
|
|
|
|
### 2. Stack Events Should Have JSON Content
|
|
|
|
The plan specifies `content: JSON.stringify({ name, description })` but the original `publish_all.py` used `content: ""`. The frontend reads name/description from tags, so this is non-critical, but it's inconsistent with the spec.
|
|
|
|
**Fixed in** [`app-stacks/publish_all.py`](../app-stacks/publish_all.py:435):
|
|
```python
|
|
"content": json.dumps({"name": stack["name"], "description": stack["description"]}),
|
|
```
|
|
|
|
### 3. `relay.zapstore.dev` Rejects Kind 30078 with `d: app-*`
|
|
|
|
The relay `wss://relay.zapstore.dev` has a whitelist for kind 30078 `d` tags — only `zapstore-device-state` and `zapstore-device-key-backup` are accepted. App definitions with `d: app-<identifier>` are rejected with `invalid 'd' tag`.
|
|
|
|
**Workaround:** App definitions are published to the other 3 relays (`nos.lol`, `primal.net`, `laantungir.net/relay`). The frontend's NDK subscription queries user-configured relays, so it will find them as long as the user has at least one of these relays configured.
|
|
|
|
### 4. Published Event Counts
|
|
|
|
| Event Type | Expected | Published | Relays |
|
|
|-----------|----------|-----------|--------|
|
|
| App Stacks (kind 30267) | 20 | 20 | All 4 relays |
|
|
| App Definitions (kind 30078) | 182 | 174 | 3 relays (not zapstore.dev) |
|
|
|
|
The 8 missing app definitions are duplicates (e.g., `com.flux` appears in both "Productivity & Notes" and "Finance & Budgeting", `com.unciv.app` in both "Utilities & Tools" and "Games"). The script overwrites the previous event with the same `d` tag, so only the last one persists.
|
|
|
|
### 5. Old Stack Had No `#t: app-stack` Tag
|
|
|
|
The previously published stack (`privacy-approved-notes`) was missing the `["t", "app-stack"]` tag entirely. All 20 new stacks include it, which is required for the frontend's subscription filter `#t: ['app-stack']` to find them.
|