Compare commits
No commits in common. "5635f7910a342a0ec9b187643b45cb7f650520d5" and "f29d4f94a05eab9ee694130cff4e1c1ddd46efa1" have entirely different histories.
5635f7910a
...
f29d4f94a0
7 changed files with 159 additions and 212 deletions
|
|
@ -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 <npub> <new-name> # 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.
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 10 KiB |
|
|
@ -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 (
|
||||
<span className={`badge badge-${tone}${className ? ` ${className}` : ''}`}>{children}</span>
|
||||
);
|
||||
export function Badge({ tone = 'neutral', children }: { tone?: BadgeTone; children: ReactNode }) {
|
||||
return <span className={`badge badge-${tone}`}>{children}</span>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
const { state, relayTest, lastPublish } = useApp();
|
||||
const [statuses, setStatuses] = useState<Record<string, { state: RelayStatus; detail?: string }>>(
|
||||
{},
|
||||
);
|
||||
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<string, { state: RelayStatus; detail?: string }> = {};
|
||||
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) {
|
|||
<div className="home-grid">
|
||||
<section className="card">
|
||||
<header className="card-header">
|
||||
<h2>Your profiles</h2>
|
||||
<h2>Active profile</h2>
|
||||
</header>
|
||||
<div className="card-body">
|
||||
{active ? (
|
||||
<div className="active-profile-row">
|
||||
<Avatar npub={active.npub} label={active.label} size="lg" />
|
||||
<div className="active-profile-meta">
|
||||
<span className="profile-name">{active.label}</span>
|
||||
<code className="mono" title={active.npub}>
|
||||
{shortenNpub(active.npub, shorten)}
|
||||
</code>
|
||||
</div>
|
||||
<CopyButton text={active.npub} label="full npub" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted">
|
||||
No profile selected.{' '}
|
||||
<button type="button" className="linklike" onClick={() => onNavigate('profiles')}>
|
||||
Choose a profile
|
||||
</button>{' '}
|
||||
to get started.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<header className="card-header">
|
||||
<h2>Relays</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onNavigate('profiles')}
|
||||
aria-label="Manage profiles"
|
||||
onClick={() => void runCheck()}
|
||||
loading={testing}
|
||||
disabled={enabled.length === 0}
|
||||
>
|
||||
<Icon name="users" size={16} />
|
||||
Manage
|
||||
<Icon name="refresh" size={16} />
|
||||
Refresh
|
||||
</Button>
|
||||
</header>
|
||||
<div className="card-body">
|
||||
<ul className="home-profile-list">
|
||||
{state?.profiles.map((profile) => (
|
||||
<li
|
||||
key={profile.npub}
|
||||
className={`home-profile-row${profile.is_active ? ' is-active' : ''}`}
|
||||
>
|
||||
<Avatar
|
||||
npub={profile.npub}
|
||||
label={profile.label}
|
||||
size="lg"
|
||||
picture={profile.picture}
|
||||
/>
|
||||
<div className="active-profile-meta">
|
||||
<span className="profile-name">
|
||||
{profile.label}
|
||||
{profile.is_active && (
|
||||
<Badge tone="success" className="home-active-badge">
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
<code className="mono" title={profile.npub}>
|
||||
{shortenNpub(profile.npub, shorten)}
|
||||
</code>
|
||||
</div>
|
||||
<div className="home-profile-actions">
|
||||
<CopyButton text={profile.npub} label="full npub" />
|
||||
{profile.is_active ? (
|
||||
<Button variant="secondary" size="sm" disabled>
|
||||
Selected
|
||||
</Button>
|
||||
{enabled.length === 0 ? (
|
||||
<p className="muted">
|
||||
No relays enabled.{' '}
|
||||
<button type="button" className="linklike" onClick={() => onNavigate('relays')}>
|
||||
Enable a relay
|
||||
</button>{' '}
|
||||
to publish notes.
|
||||
</p>
|
||||
) : testing && Object.keys(statuses).length === 0 ? (
|
||||
<Spinner label="Testing connections…" />
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
loading={selecting === profile.npub}
|
||||
onClick={() => void onSelect(profile.npub)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ul className="relay-status-list">
|
||||
{enabled.map((relay) => {
|
||||
const status = statuses[relay.url];
|
||||
return (
|
||||
<li key={relay.url} className="relay-status-row">
|
||||
<span
|
||||
className={`status-dot ${
|
||||
status?.state === 'connected'
|
||||
? 'is-ok'
|
||||
: status?.state === 'failed'
|
||||
? 'is-bad'
|
||||
: 'is-pending'
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="mono relay-url">{relay.url}</span>
|
||||
<span className="relay-status-text">
|
||||
{status?.state === 'connected'
|
||||
? `Connected${status.detail ? ` · ${status.detail}` : ''}`
|
||||
: status?.state === 'failed'
|
||||
? 'Unavailable'
|
||||
: 'Checking…'}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={onCreateProfile}
|
||||
className="home-add-profile"
|
||||
>
|
||||
<Icon name="plus" size={16} />
|
||||
Add profile
|
||||
</Button>
|
||||
)}
|
||||
{failedCount > 0 &&
|
||||
!testing &&
|
||||
enabled.length > 0 &&
|
||||
Object.keys(statuses).length === enabled.length && (
|
||||
<p className="muted small">
|
||||
{connectedCount} of {enabled.length} enabled relays reachable.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(<HomeScreen onNavigate={vi.fn()} onCreateProfile={vi.fn()} />);
|
||||
|
||||
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(<HomeScreen onNavigate={vi.fn()} onCreateProfile={vi.fn()} />);
|
||||
|
||||
expect(await screen.findAllByText(/Connected/)).not.toHaveLength(0);
|
||||
expect(screen.getAllByText('wss://relay.damus.io')).not.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import type { UserConfig } from 'vite';
|
|||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue