# Architecture: marstemedia marstemedia is a desktop (and Android) application built with **Tauri v2**. The UI is written in Rust compiled to WebAssembly using **Yew**, and the backend logic runs as native Rust code inside the Tauri host process. --- ## Big Picture ``` ┌─────────────────────────────────────────────────────────┐ │ Tauri Desktop App │ │ │ │ ┌──────────────────────┐ IPC (invoke) ┌──────────┐ │ │ │ Frontend (Wasm) │ ──────────────► │ Backend │ │ │ │ Yew / Rust │ ◄────────────── │ Rust │ │ │ │ src/ │ JSON results │ src-tauri│ │ │ └──────────────────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘ ▲ renders inside a webview (WebKit/Edge) ``` The frontend never touches the network directly for heavy operations — it sends named commands via `window.__TAURI__.core.invoke()` to the backend, which does the actual networking, file I/O, and cryptography. --- ## Frontend (`src/`) Built with **Yew 0.21** (React-style component model for Rust/Wasm) and **yew-router** for client-side routing. ### Entry point | File | Role | |---|---| | `src/main.rs` | Bootstraps the Wasm app, mounts the `App` component | | `src/app.rs` | Defines the `Route` enum and the top-level layout (router + navbar) | ### Routes | Path | Component | File | |---|---|---| | `/` | `Home` | `src/pages/home.rs` | | `/news` | `News` | `src/pages/news.rs` | | `/greet` | `Greet` | `src/pages/greet.rs` | ### Components - **`Navbar`** (`src/navbar/navbar.rs`) — Fixed bottom dock with links to all three routes. Reads the current route via `use_route` to highlight the active link. - **`Home`** (`src/pages/home.rs`) — Nostr feed. Manages hashtag filter state, invokes `fetch_nostr_posts` on load and on user interaction, renders posts as `PostCard` components. - **`News`** (`src/pages/news.rs`) — Dual-mode news page. Toggles between AI mode (Groq / OpenRouter) and RSS mode. Has a collapsible config panel for API keys and RSS feed management. Renders articles as `NewsCard` with Markdown support via `pulldown-cmark`. ### How the frontend talks to the backend Every backend call uses the `invoke` wasm-bindgen extern: ```rust #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)] async fn invoke(cmd: &str, args: JsValue) -> Result; ``` Arguments are serialized with `serde-wasm-bindgen` and results are deserialized back into typed Rust structs. --- ## Backend (`src-tauri/src/`) Native Rust code that runs in the Tauri host process. Registered commands are the only API surface the frontend can call. ### Entry point | File | Role | |---|---| | `src-tauri/src/main.rs` | Desktop executable entry point, calls `run()` | | `src-tauri/src/lib.rs` | Configures Tauri: registers `NewsState`, registers all commands | ### Modules #### `home.rs` — Nostr Handles the `fetch_nostr_posts` command. 1. Generates a fresh, random throwaway keypair on every call (privacy by design — no persistent identity). 2. Creates an `EasyNostr` client using the temp key (via the local `easy-nostr` library). 3. Connects to three relays: `relay.damus.io`, `nos.lol`, `relay.snort.social`. 4. Fetches posts by hashtag list, or random posts if no tags are given. 5. Maps results to the serializable `LocalPost` struct and returns JSON. #### `news.rs` — RSS & AI News Manages `NewsState` (held in Tauri's managed state, shared across calls): ``` NewsState { openrouter_key: Mutex groq_key: Mutex rss_config: Mutex } ``` **Commands:** | Command | What it does | |---|---| | `load_rss_config` | Reads `rss.ron` from the app config dir; writes 16 default feeds if the file is missing or has fewer than 3 entries | | `save_rss_urls` | Serializes the feed list to `rss.ron` and updates in-memory state | | `save_openrouter_key` | Stores the key in `NewsState.openrouter_key` (in-memory only, not persisted to disk) | | `save_groq_key` | Same for the Groq key | | `fetch_ai_news` | POSTs to Groq or OpenRouter chat completions API; asks for a short tech news summary in German Markdown | | `fetch_rss_news` | Iterates the in-memory feed list, fetches each feed with `reqwest`, parses with `feed-rs`, takes up to 3 articles per feed, returns `Vec` | ### Local library: `easy-nostr` Located at `src-tauri/easy-nostr/` (path dependency). Wraps `nostr-sdk` to provide high-level helpers like `get_random_posts()` and `get_posts_by_hashtags()`. Written by the same author (Bytemalte). --- ## Configuration & Persistence | What | Where | Format | |---|---|---| | RSS feed list | `/rss.ron` | RON | | API keys | In-memory (`NewsState`) — lost on restart | — | | App window config | `src-tauri/tauri.conf.json` | JSON | API keys are intentionally not persisted to disk. The user re-enters them each session via the config panel. --- ## Build System | Tool | Role | |---|---| | `trunk` | Builds and bundles the Yew/Wasm frontend (`trunk build` / `trunk serve`) | | `tauri-cli` | Wraps the full build and produces the native app bundle | | `Trunk.toml` | Trunk configuration (output dir `dist/`, used by Tauri) | | `justfile` | Developer shortcuts | The `tauri.conf.json` wires them together: ``` beforeDevCommand: trunk serve (starts dev server on :1420) beforeBuildCommand: trunk build (builds Wasm) frontendDist: ../dist (Tauri embeds this) ``` ### CI / Android `.gitea/workflows/android.yaml` runs on push to `main` or version tags. It: 1. Clones `easy-nostr` as a private submodule. 2. Installs Rust with four Android targets + `wasm32-unknown-unknown`. 3. Installs Trunk (binary) and `tauri-cli` (cargo). 4. Sets up Android SDK + NDK 25. 5. Runs `cargo tauri android build`. --- ## Data Flow Summary ``` User action │ ▼ Yew component (state update) │ invoke("command_name", args_json) ▼ Tauri IPC bridge │ ▼ Backend Rust function │ network / file I/O ▼ External service (Nostr relay / RSS feed / AI API / disk) │ serialized result ▼ Yew component re-renders ```