Add docs for new non-WS message fetching and remove dead funcs

This commit is contained in:
Jack Kingsman
2026-02-04 11:59:12 -08:00
parent 122109dc58
commit edfc95a2e2
5 changed files with 20 additions and 78 deletions

View File

@@ -228,6 +228,7 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`).
| POST | `/api/packets/maintenance` | Delete old packets (cleanup) |
| POST | `/api/contacts/{key}/mark-read` | Mark contact conversation as read |
| POST | `/api/channels/{key}/mark-read` | Mark channel as read |
| GET | `/api/read-state/unreads` | Server-computed unread counts, mentions, last message times |
| POST | `/api/read-state/mark-all-read` | Mark all conversations as read |
| GET | `/api/settings` | Get app settings |
| PATCH | `/api/settings` | Update app settings |
@@ -266,7 +267,7 @@ Read state (`last_read_at`) is tracked **server-side** for consistency across de
- Stored as Unix timestamp in `contacts.last_read_at` and `channels.last_read_at`
- Updated via `POST /api/contacts/{key}/mark-read` and `POST /api/channels/{key}/mark-read`
- Bulk update via `POST /api/read-state/mark-all-read`
- Frontend compares `last_read_at` with message `received_at` to count unreads
- Aggregated counts via `GET /api/read-state/unreads` (server-side computation)
**State Tracking Keys (Frontend)**: Generated by `getStateKey()` for message times (sidebar sorting):
- Channels: `channel-{channel_key}`

View File

@@ -35,7 +35,7 @@ app/
├── channels.py # Channel CRUD, radio sync, mark-read
├── messages.py # Message list and send (direct/channel)
├── packets.py # Raw packet endpoints, historical decryption
├── read_state.py # Bulk read state operations (mark-all-read)
├── read_state.py # Read state: unread counts, mark-all-read
├── settings.py # App settings (max_radio_contacts)
└── ws.py # WebSocket endpoint at /api/ws
```
@@ -104,6 +104,10 @@ await ws_manager.broadcast("message", {"id": 1, "text": "Hello"})
Event types: `health`, `contacts`, `channels`, `message`, `contact`, `raw_packet`, `message_acked`, `error`
**Note:** The WebSocket initial connect only sends `health`. Contacts and channels are fetched
via REST (`GET /api/contacts`, `GET /api/channels`) for faster parallel loading. The WS still
broadcasts real-time `contacts`/`channels` updates when data changes.
Helper functions for common broadcasts:
```python
@@ -490,6 +494,7 @@ All endpoints are prefixed with `/api`.
- `DELETE /api/channels/{key}` - Delete channel
### Read State
- `GET /api/read-state/unreads?name=X` - Server-computed unread counts, mention flags, and last message times
- `POST /api/read-state/mark-all-read` - Mark all contacts and channels as read
### Messages

View File

@@ -497,59 +497,6 @@ class MessageRepository:
acked=row["acked"],
)
@staticmethod
async def get_bulk(
conversations: list[dict],
limit_per_conversation: int = 100,
) -> dict[str, list["Message"]]:
"""Fetch messages for multiple conversations in one query per conversation.
Args:
conversations: List of {type: 'PRIV'|'CHAN', conversation_key: string}
limit_per_conversation: Max messages to return per conversation
Returns:
Dict mapping 'type:conversation_key' to list of messages
"""
result: dict[str, list[Message]] = {}
for conv in conversations:
msg_type = conv.get("type")
conv_key = conv.get("conversation_key")
if not msg_type or not conv_key:
continue
key = f"{msg_type}:{conv_key}"
cursor = await db.conn.execute(
"""
SELECT * FROM messages
WHERE type = ? AND conversation_key LIKE ?
ORDER BY received_at DESC
LIMIT ?
""",
(msg_type, f"{conv_key}%", limit_per_conversation),
)
rows = await cursor.fetchall()
result[key] = [
Message(
id=row["id"],
type=row["type"],
conversation_key=row["conversation_key"],
text=row["text"],
sender_timestamp=row["sender_timestamp"],
received_at=row["received_at"],
paths=MessageRepository._parse_paths(row["paths"]),
txt_type=row["txt_type"],
signature=row["signature"],
outgoing=bool(row["outgoing"]),
acked=row["acked"],
)
for row in rows
]
return result
@staticmethod
async def get_unread_counts(name: str | None = None) -> dict:
"""Get unread message counts, mention flags, and last message times for all conversations.

View File

@@ -38,19 +38,6 @@ async def list_messages(
)
@router.post("/bulk", response_model=dict[str, list[Message]])
async def get_messages_bulk(
conversations: list[dict],
limit_per_conversation: int = Query(default=100, ge=1, le=1000),
) -> dict[str, list[Message]]:
"""Fetch messages for multiple conversations in one request.
Body should be a list of {type: 'PRIV'|'CHAN', conversation_key: string}.
Returns a dict mapping 'type:conversation_key' to list of messages.
"""
return await MessageRepository.get_bulk(conversations, limit_per_conversation)
@router.post("/direct", response_model=Message)
async def send_direct_message(request: SendDirectMessageRequest) -> Message:
"""Send a direct message to a contact."""

View File

@@ -90,10 +90,13 @@ The `preferences_migrated` flag prevents duplicate migrations.
### State Flow
1. **WebSocket** pushes real-time updates (health, contacts, channels, messages)
2. **REST API** fetches initial data and handles user actions
1. **REST API** fetches initial data on mount in parallel (config, settings, channels, contacts, unreads)
2. **WebSocket** pushes real-time updates (health, messages, contact changes, raw packets)
3. **Components** receive state as props, call handlers to trigger changes
**Note:** Contacts and channels are loaded via REST on mount (not from WebSocket initial push).
The WebSocket only sends health on initial connect, then broadcasts real-time updates.
### Conversation Header
For contacts, the header shows path information alongside "Last heard":
@@ -229,8 +232,8 @@ interface Message {
}
interface Conversation {
type: 'contact' | 'channel' | 'raw' | 'map';
id: string; // PublicKey for contacts, ChannelKey for channels, 'raw'/'map' for special views
type: 'contact' | 'channel' | 'raw' | 'map' | 'visualizer';
id: string; // PublicKey for contacts, ChannelKey for channels, 'raw'/'map'/'visualizer' for special views
name: string;
}
@@ -397,11 +400,8 @@ for local state tracking, while `conversation_key` is the raw database field.
Unread tracking uses server-side `last_read_at` timestamps for cross-device consistency:
```typescript
// Contacts and channels include last_read_at from server
interface Contact {
// ...
last_read_at: number | null; // Unix timestamp when conversation was last read
}
// Fetch aggregated unread counts from server (replaces bulk message fetch + client-side counting)
await api.getUnreads(myName); // Returns { counts, mentions, last_message_times }
// Mark as read via API (called automatically when viewing conversation)
await api.markContactRead(publicKey);
@@ -409,7 +409,9 @@ await api.markChannelRead(channelKey);
await api.markAllRead(); // Bulk mark all as read
```
Unread count = messages where `received_at > last_read_at`.
The `useUnreadCounts` hook fetches counts from `GET /api/read-state/unreads` on mount and
when channels/contacts change. Real-time increments are still tracked client-side via WebSocket
message events. The server computes unread counts using `last_read_at` vs `received_at`.
## Utility Functions