diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index 643445a..81923f2 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -6,14 +6,13 @@ verified green at the moment this file was written. ## Where things are - Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master` @ `835b0a0` ("frontend: list all profiles on Home, pin - dev host, widen profiles card"). Before it: `71e580f` (Trim white margins - from sidebar logo artwork), `f29d4f9` (Checkpoint 0.45 migration), - `fa5ba08` (Security: upgrade nostr stack 0.40->0.45), `bf10ae3`, `e210f17`, - `7098e75`, `e6a1efc`, `6d5063d`, `1c428a9`. -- Working tree: still DIRTY with the long-standing user changes — `concept3.svg` - deleted, `KeynectrAppIconPossibility02.jpeg` untracked. These are NOT part - of the frontend commit above and remain uncommitted. +- Git repo: `master` @ `fa5ba08` ("Security: upgrade nostr stack 0.40->0.45 + clearing 11 RustSec advisories"). Before it: `bf10ae3` (Checkpoint: clarify + NIP-42 status), `e210f17` (Checkpoint: DRY modal save lifecycle / R3), + `7098e75` (refactor: share modal save lifecycle), `e6a1efc` (R4), + `6d5063d` (R2), `1c428a9` (R1). +- Working tree: only the long-standing user changes remain — `concept3.svg` + deleted, `KeynectrAppIconPossibility02.jpeg` untracked. ## What was completed @@ -42,7 +41,6 @@ verified green at the moment this file was written. ## Commits added most recently -- `835b0a0` frontend: list all profiles on Home, pin dev host, widen profiles card - `fa5ba08` Security: upgrade nostr stack 0.40->0.45 clearing 11 RustSec advisories ## Verification commands run (all green) @@ -1351,76 +1349,3 @@ $B rename # rename + publish the new name - Relay config at time of writing (all enabled): nos.lol, relay.primal.net, l484.com (user's own), damus.io, snort.social, soloco.nl; relay.nostr.band added but was unreachable (handshake timeout) — retry enabling later. - -## Checkpoint — frontend home screen improvements (2026-08-26) - -A stopping point you can return to if this session is closed. - -### Where things are - -- Project: `/home/avi/Projects/Nostr_Keynctr` -- Git repo: `master`. **Working tree is DIRTY** — the changes below are local - edits and have NOT been committed yet (see "outstanding items"). -- Dev-run fix also applied: `frontend/vite.config.ts` now binds the dev server - to `host: '127.0.0.1'` so Electron (`NOSTR_GUI_DEV_URL=http://127.0.0.1:5173`) - and Vite agree on address and the window is no longer a white box. - -### What was completed (user-facing) - -1. **Profile pictures now show on Home.** The Home screen's active-profile - `Avatar` was missing the `picture` prop, so the image only appeared on the - Profiles screen. `HomeScreen.tsx` now passes `picture={profile.picture}`. -2. **All profiles listed on Home.** Replaced the single "Active profile" card - with a "Your profiles" card listing every profile (avatar + picture, label, - shortened npub, Active badge). Each non-active row has a working **Select** - button (calls `selectProfile`); a **Manage** link goes to the Profiles - screen and an **Add profile** button is at the bottom. -3. **`Badge` now accepts `className`** so the Active badge can be inline next to - the name. -4. **Relays card removed from Home.** The "Relays" card (live connection status, - refresh, "enable a relay" link) is no longer shown on Home — relay status - lives on the Relays screen. Removed the now-unused relay-check logic - (`relayTest`/`runCheck`/`statuses`/`testing`/`enabled`/`Spinner`) and the - corresponding test. -5. **Profile card now full-width on Home.** `.home-grid` was a two-column grid - (profiles + relays); with Relays gone the profiles card only filled the left - half. Changed it to a single `1fr` column and dropped the now-redundant - `@media (max-width: 900px)` override, so "Your profiles" matches the width - of "Most recent publication" below it. No JSX change. - -### Files changed - -- `frontend/src/screens/HomeScreen.tsx` (profiles list + picture prop + select; relays removed) -- `frontend/src/components/Badge.tsx` (add `className` prop) -- `frontend/src/styles.css` (`.home-profile-list` / `.home-profile-row` / etc.) -- `frontend/src/test/HomeScreen.test.tsx` (scope copy-button test to Alice's row; drop relay-status test) -- `frontend/vite.config.ts` (dev server `host: '127.0.0.1'`) - -### Verification commands run (all green) - -Frontend (`frontend/`): `npm run typecheck` clean; `npm run lint` clean; -`npm test` 15 files / 98 tests pass; `npm run format:check` clean (after -`npm run format`). - -### How to use / reproduce - -Dev mode (two terminals, from `~/Projects/Nostr_Keynctr/frontend`): - -``` -# Terminal 1 -npm run dev # prints Local: http://127.0.0.1:5173/ - -# Terminal 2 -NOSTR_GUI_DEV_URL=http://127.0.0.1:5173 npm start -``` - -Home now shows every profile with its picture, and selecting one there updates -the active profile immediately. - -### Outstanding items - -- Committed as `835b0a0`. The working tree still has unrelated user changes - (`concept3.svg` deleted, `KeynectrAppIconPossibility02.jpeg` untracked) that - were intentionally left out of the commit. -- The dev-server `host: '127.0.0.1'` change is intentional; if you later need - LAN access to the dev server, set it back to `true` and use the matching URL. diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png index fcb48ba..d02f1d6 100644 Binary files a/frontend/src/assets/logo.png and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/components/Badge.tsx b/frontend/src/components/Badge.tsx index 265b6a7..b0c3748 100644 --- a/frontend/src/components/Badge.tsx +++ b/frontend/src/components/Badge.tsx @@ -2,16 +2,6 @@ import type { ReactNode } from 'react'; export type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info'; -export function Badge({ - tone = 'neutral', - className, - children, -}: { - tone?: BadgeTone; - className?: string; - children: ReactNode; -}) { - return ( - {children} - ); +export function Badge({ tone = 'neutral', children }: { tone?: BadgeTone; children: ReactNode }) { + return {children}; } diff --git a/frontend/src/screens/HomeScreen.tsx b/frontend/src/screens/HomeScreen.tsx index da4a7f3..31c38a3 100644 --- a/frontend/src/screens/HomeScreen.tsx +++ b/frontend/src/screens/HomeScreen.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert } from '../components/Alert'; import { Avatar } from '../components/Avatar'; import { Badge } from '../components/Badge'; @@ -6,27 +6,67 @@ import { Button } from '../components/Button'; import { CopyButton } from '../components/CopyButton'; import { EmptyState } from '../components/EmptyState'; import { Icon } from '../components/Icon'; +import { Spinner } from '../components/Spinner'; import { shortenNpub } from '../lib/format'; import type { Screen } from '../lib/navigation'; import { useApp } from '../state/AppProvider'; +type RelayStatus = 'checking' | 'connected' | 'failed'; + interface HomeScreenProps { onNavigate: (screen: Screen) => void; onCreateProfile: () => void; } export function HomeScreen({ onNavigate, onCreateProfile }: HomeScreenProps) { - const { state, lastPublish, selectProfile } = useApp(); - const [selecting, setSelecting] = useState(null); + const { state, relayTest, lastPublish } = useApp(); + const [statuses, setStatuses] = useState>( + {}, + ); + const [testing, setTesting] = useState(false); - const onSelect = async (npub: string) => { - setSelecting(npub); - try { - await selectProfile(npub); - } finally { - setSelecting(null); + const enabled = useMemo( + () => state?.settings.relays.filter((r) => r.enabled) ?? [], + [state?.settings.relays], + ); + + const runCheck = useCallback(async () => { + setTesting(true); + const entries: Record = {}; + for (const relay of enabled) { + entries[relay.url] = { state: 'checking' }; } - }; + setStatuses({ ...entries }); + await Promise.all( + enabled.map(async (relay) => { + try { + const result = await relayTest(relay.url); + entries[relay.url] = { + state: 'connected', + detail: result.latency_ms != null ? `${result.latency_ms} ms` : 'connected', + }; + } catch (error) { + entries[relay.url] = { + state: 'failed', + detail: error instanceof Error ? error.message : String(error), + }; + } + }), + ); + setStatuses({ ...entries }); + setTesting(false); + }, [enabled, relayTest]); + + useEffect(() => { + if (enabled.length > 0) { + void runCheck(); + } else { + setStatuses({}); + } + }, [enabled, runCheck]); + + const connectedCount = Object.values(statuses).filter((s) => s.state === 'connected').length; + const failedCount = Object.values(statuses).filter((s) => s.state === 'failed').length; const active = state?.active_profile ?? null; const shorten = state?.settings.shorten_npub ?? true; @@ -79,72 +119,94 @@ export function HomeScreen({ onNavigate, onCreateProfile }: HomeScreenProps) {
-

Your profiles

+

Active profile

+
+
+ {active ? ( +
+ +
+ {active.label} + + {shortenNpub(active.npub, shorten)} + +
+ +
+ ) : ( +

+ No profile selected.{' '} + {' '} + to get started. +

+ )} +
+
+ +
+
+

Relays

-
    - {state?.profiles.map((profile) => ( -
  • - -
    - - {profile.label} - {profile.is_active && ( - - Active - - )} - - - {shortenNpub(profile.npub, shorten)} - -
    -
    - - {profile.is_active ? ( - - ) : ( - - )} -
    -
  • - ))} -
- + {enabled.length === 0 ? ( +

+ No relays enabled.{' '} + {' '} + to publish notes. +

+ ) : testing && Object.keys(statuses).length === 0 ? ( + + ) : ( +
    + {enabled.map((relay) => { + const status = statuses[relay.url]; + return ( +
  • +
  • + ); + })} +
+ )} + {failedCount > 0 && + !testing && + enabled.length > 0 && + Object.keys(statuses).length === enabled.length && ( +

+ {connectedCount} of {enabled.length} enabled relays reachable. +

+ )}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 1dd6c1d..947ee5e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1003,10 +1003,16 @@ select { .home-grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: 1fr 1fr; gap: 20px; } +@media (max-width: 900px) { + .home-grid { + grid-template-columns: 1fr; + } +} + .active-profile-row { display: flex; align-items: center; @@ -1061,50 +1067,6 @@ select { object-fit: cover; } -.home-profile-list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 10px; -} - -.home-profile-row { - display: flex; - align-items: center; - gap: 14px; - padding: 8px; - border-radius: 8px; - border: 1px solid transparent; -} - -.home-profile-row.is-active { - background: var(--token-item-bg, rgba(0, 0, 0, 0.04)); - border-color: var(--token-border, rgba(0, 0, 0, 0.12)); -} - -.home-profile-row .active-profile-meta { - flex: 1; - min-width: 0; -} - -.home-profile-actions { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; -} - -.home-active-badge { - margin-left: 8px; - vertical-align: middle; -} - -.home-add-profile { - margin-top: 14px; -} - .relay-status-list { list-style: none; margin: 0; diff --git a/frontend/src/test/HomeScreen.test.tsx b/frontend/src/test/HomeScreen.test.tsx index 516c4b1..8b8572d 100644 --- a/frontend/src/test/HomeScreen.test.tsx +++ b/frontend/src/test/HomeScreen.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor, within } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { HomeScreen } from '../screens/HomeScreen'; import { renderWithApp } from './render'; @@ -36,8 +36,8 @@ describe('HomeScreen', () => { renderHome(backend); renderWithApp(); - const aliceRow = (await screen.findByText('Alice')).closest('.home-profile-row') as HTMLElement; - await userEvent.setup().click(within(aliceRow).getByRole('button', { name: 'Copy full npub' })); + await screen.findByText('Alice'); + await userEvent.setup().click(screen.getByRole('button', { name: 'Copy full npub' })); await waitFor(() => { expect(backend.copied).toContain(ALICE); }); @@ -54,4 +54,13 @@ describe('HomeScreen', () => { .click(screen.getByRole('button', { name: /Create your first profile/i })); expect(onCreateProfile).toHaveBeenCalled(); }); + + it('shows live relay connection status from the backend', async () => { + const backend = createFakeBackend(); + renderHome(backend); + renderWithApp(); + + expect(await screen.findAllByText(/Connected/)).not.toHaveLength(0); + expect(screen.getAllByText('wss://relay.damus.io')).not.toHaveLength(0); + }); }); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4983c5f..d87dd77 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,7 +6,6 @@ import type { UserConfig } from 'vite'; export default defineConfig({ plugins: [react()], server: { - host: '127.0.0.1', port: 5173, strictPort: true, },