Skip to content

developit/mastodawn

Repository files navigation

Mastodawn

A lightweight, privacy-focused Mastodon client built with Preact and Signals.

Live: mastodawn.com

Mastodawn icon

Why?

Mastodawn was built as a reference implementation for an ultra-fast Preact Single Page Application with full Signals-based rendering and reactivity. It demonstrates how to build a complex, real-world PWA that stays lightweight and responsive.

Highlights

  • ~50KB application bundle (gzip)
  • Full Signals reactivity — no Context, no reducers, just reactive state
  • Offline-first with Service Worker caching and streaming updates
  • Installable PWA with push notifications, share target, and app shortcuts
  • Privacy-focused — all data stays on your device; communicates directly with Mastodon instances

Tech Stack

Layer Technology
UI Framework Preact 10
State Management @preact/signals
Routing preact-iso
Build Tool Vite 4
Type Checking TypeScript (JSDoc)
Styling Tailwind CSS 3 + DaisyUI
Icons Iconify (compile-time inlined)

Architecture

Signals-Based State

Mastodawn avoids Context and reducers entirely. State is modeled as signals — fine-grained reactive values that trigger re-renders only where their .value is read:

// API responses are wrapped in signals for reactive updates
const timeline = signal([]);

// WebSocket stream updates mutate the signal directly
stream.on('update', status => {
  timeline.value = [status, ...timeline.value];
});

// Components re-render automatically when timeline.value changes
function Timeline() {
  return <For each={timeline}>{post => <Post post={post} />}</For>;
}

Key patterns used throughout the codebase (src/lib/utils.jsx):

  • <For each={signal}> — efficient keyed list rendering from a signal
  • <Show when={signal}> — conditional rendering driven by a signal
  • asyncComputed() — derived async state (fetch-on-signal-change with Suspense)
  • useSignalRef() — a signal that doubles as a DOM ref

Service Worker

A custom Vite plugin compiles the Service Worker (src/sw/) with a versioned asset manifest injected at build time:

  • Install: precaches the app shell and all hashed assets
  • Fetch: cache-first for assets, network-first for HTML
  • Share Target: intercepts POST /compose for the Web Share Target API
  • Push: displays native notifications and routes taps to the app

Mastodon API Client

The API client (src/lib/masto.js) is a custom ~1,500-line module with:

  • URL-template-based request definitions ("GET /statuses/{id}")
  • In-flight request deduplication
  • Cache API integration with TTL-based invalidation
  • Signal-wrapped responses for reactive UI updates
  • WebSocket streaming with automatic reconnection (exponential backoff)
  • Multi-instance support (connect to multiple Mastodon servers)

OAuth Flow

Authentication uses a popup-based OAuth flow:

  1. App opens a popup to the Mastodon instance's OAuth endpoint
  2. Instance redirects to /oauth-callback.html with an authorization code
  3. Callback page posts the code back to the opener via postMessage
  4. App exchanges the code for an access token — stored locally, never sent to any backend

Getting Started

Prerequisites

Install & Run

# Install dependencies
npm install

# Start dev server (HTTPS, required for Service Worker)
npm run dev

# Type-check
npm run check

# Production build
npm run build

# Preview production build
npm run preview

The dev server starts on https://localhost:5173 with HTTPS enabled via mkcert (you may need to accept the self-signed certificate).

Scripts

Command Description
npm run dev Start Vite dev server with HTTPS and HMR
npm run build Production build to dist/
npm run check Run TypeScript type checking
npm run preview Serve the production build locally

Project Structure

src/
├── index.jsx              # Entry point (hydrate + prerender)
├── style.css              # Global styles (Tailwind)
├── components/
│   ├── app.jsx            # Root component with lazy-loaded routes
│   ├── masto.jsx          # Mastodon client singleton + signal state
│   ├── timeline.jsx       # Feed rendering with virtual scrolling
│   ├── post.jsx           # Status/post renderer
│   ├── compose.jsx        # Post composer with media upload
│   ├── header.jsx         # Navigation header
│   ├── service-worker-manager.jsx
│   └── ...                # Media viewer, GIF picker, etc.
├── routes/                # Page components (lazy-loaded)
│   ├── home.jsx           # Home timeline
│   ├── explore.jsx        # Discover content
│   ├── profile.jsx        # User profiles
│   ├── notifications.jsx  # Notification feed
│   ├── messages.jsx       # Direct messages
│   ├── compose.jsx        # Compose page
│   ├── bookmarks.jsx      # Saved posts
│   ├── settings.jsx       # App settings
│   └── ...
├── lib/
│   ├── masto.js           # Mastodon API client + WebSocket streaming
│   ├── utils.jsx          # Signal utilities (For, Show, asyncComputed)
│   ├── sw.js              # Service Worker RPC helpers
│   └── ...
└── sw/
    └── index.js           # Service Worker implementation

Deployment

Mastodawn is a static SPA. The production build outputs to dist/ and can be deployed to any static hosting provider.

Netlify (current deployment): the included netlify.toml handles SPA routing.

For other hosts, configure a catch-all redirect so all routes serve index.html:

# nginx example
location / {
  try_files $uri /index.html;
}

Contributing

Contributions are welcome! This project serves primarily as a reference implementation, but bug fixes and improvements are appreciated.

  1. Fork the repository
  2. Create your branch (git checkout -b my-fix)
  3. Commit your changes (git commit -m 'Fix something')
  4. Push (git push origin my-fix)
  5. Open a Pull Request

License

MIT — Jason Miller

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors