Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# noxtr
# nox

Nostr SPA built with TypeScript + Vite.
nox is a relay-first SPA built with TypeScript + Vite.
This README is for developers working on this repository.

## Stack
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nostr-spa",
"name": "nox",
"version": "1.0.0",
"description": "Simple Nostr SPA",
"description": "nox relay-first client",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src/common/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function setEventMeta(
event: { id: string; content: string; created_at: number; pubkey: string },
npub: Npub,
): void {
const title: string = `noxtr - Event ${event.id.slice(0, 8)}`;
const title: string = `nox - Event ${event.id.slice(0, 8)}`;
const description: string =
event.content.length > 140
? `${event.content.slice(0, 140)}...`
Expand Down
18 changes: 18 additions & 0 deletions src/common/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export function setupNavigation(options: NavigationOptions): void {
document.getElementById('nav-reactions');
const relaysButton: HTMLElement | null =
document.getElementById('nav-relays');
const profileLink: HTMLAnchorElement | null = document.getElementById(
'nav-profile',
) as HTMLAnchorElement | null;
const settingsButton: HTMLElement | null =
document.getElementById('nav-settings');
const aboutButton: HTMLElement | null = document.getElementById('nav-about');
Expand Down Expand Up @@ -186,6 +189,21 @@ export function setupNavigation(options: NavigationOptions): void {
);
}

if (profileLink) {
profileLink.addEventListener('click', (event: MouseEvent): void => {
const href: string | null = profileLink.getAttribute('href');
if (!href || !href.startsWith('/')) {
closeMobileMenu();
return;
}

event.preventDefault();
wrapNavigationHandler((): void => {
options.navigateTo(href);
})();
});
}

if (relaysButton) {
relaysButton.addEventListener(
'click',
Expand Down
14 changes: 7 additions & 7 deletions src/features/about/about-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function loadAboutPage(options: AboutPageOptions): void {
const postsHeader: HTMLElement | null =
document.getElementById('posts-header');
if (postsHeader) {
postsHeader.textContent = 'About noxtr';
postsHeader.textContent = 'About nox';
postsHeader.style.display = '';
}

Expand All @@ -56,16 +56,16 @@ export function loadAboutPage(options: AboutPageOptions): void {
options.output.innerHTML = `
<article class="space-y-6 text-sm text-gray-700 leading-relaxed">
<section class="bg-white border border-gray-200 rounded-lg p-5">
<h3 class="text-lg font-bold text-gray-900 mb-2">A Practical Nostr Client</h3>
<h3 class="text-lg font-bold text-gray-900 mb-2">A Practical Relay Client</h3>
<p>
noxtr is built as a fast single-page web client focused on reliability and day-to-day use.
nox is built as a fast single-page web client focused on reliability and day-to-day use.
It keeps the protocol visible, avoids heavy abstractions, and gives you direct control over
relays, identity, and timelines.
</p>
</section>

<section class="bg-indigo-50 border border-indigo-200 rounded-lg p-5">
<h3 class="text-base font-bold text-indigo-900 mb-3">What Makes noxtr Different</h3>
<h3 class="text-base font-bold text-indigo-900 mb-3">What Makes nox Different</h3>
<ul class="space-y-2 list-disc list-inside">
<li><span class="font-semibold">Relay-first controls:</span> full relay list management, health checks, and one-click post broadcast to newly added relays.</li>
<li><span class="font-semibold">Protocol-forward rendering:</span> native support for NIP-30 custom emoji in posts, reactions, and profile metadata.</li>
Expand Down Expand Up @@ -93,16 +93,16 @@ export function loadAboutPage(options: AboutPageOptions): void {
<section class="bg-gray-50 border border-gray-200 rounded-lg p-5">
<h3 class="text-base font-bold text-gray-900 mb-2">Design Goal</h3>
<p>
noxtr prioritizes transparency over magic: when something happens on the network, you can
nox prioritizes transparency over magic: when something happens on the network, you can
usually trace it in the UI. The goal is a client that stays simple enough to trust while
still being capable enough for serious Nostr usage.
still being capable enough for serious daily use.
</p>
</section>

<section class="bg-emerald-50 border border-emerald-200 rounded-lg p-5">
<h3 class="text-base font-bold text-emerald-900 mb-2">Donate / Zap</h3>
<p>
If you find noxtr useful, you can support development via Lightning Address:
If you find nox useful, you can support development via Lightning Address:
</p>
<div class="mt-3 flex items-center gap-2 flex-wrap">
<code class="px-2 py-1 bg-white border border-emerald-200 rounded font-mono text-emerald-900 text-xs">
Expand Down
89 changes: 66 additions & 23 deletions src/features/home/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ interface ShowInputFormOptions {
handleRoute: () => void;
}

interface WindowWithNostr extends Window {
nostr?: {
getPublicKey: () => Promise<string>;
};
}

export async function showInputForm(
options: ShowInputFormOptions,
): Promise<void> {
Expand All @@ -29,30 +35,66 @@ export async function showInputForm(
}

options.output.innerHTML = `
<div class="text-center py-12">
<h2 class="text-2xl font-bold text-gray-800 mb-4">Welcome to noxtr</h2>
<p class="text-gray-600 mb-6">Connect your Nostr extension or use your private key to view your home timeline,<br/>or explore the global timeline.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
<button id="welcome-login" class="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-semibold py-3 px-6 rounded-lg transition-colors shadow-lg">
🔑 Connect Extension
</button>
<button id="welcome-global" class="bg-gradient-to-r from-slate-800 via-indigo-900 to-purple-950 hover:from-slate-900 hover:via-indigo-950 hover:to-purple-950 text-white font-semibold py-3 px-6 rounded-lg transition-colors shadow-lg">
🌍 View Global Timeline
</button>
<section class="nox-welcome py-4 sm:py-8">
<div class="nox-welcome-hero">
<div>
<p class="nox-kicker">Home Feed Access</p>
<h2 class="nox-welcome-title">Enter nox mode</h2>
<p class="nox-welcome-copy">
Sign in with a compatible extension for the cleanest flow, use a local private key if
you need direct control, or skip straight to the global timeline and watch the network
in motion.
</p>
</div>

<div class="nox-welcome-grid">
<div class="nox-feature-card">
<strong>Direct relay view</strong>
<span>Posts come from your configured relay set, not from an app server in the middle.</span>
</div>
<div class="nox-feature-card">
<strong>Cache-first rendering</strong>
<span>IndexedDB keeps feeds, profiles, and timelines close for faster revisits.</span>
</div>
<div class="nox-feature-card">
<strong>Protocol-visible UI</strong>
<span>Relay management, reply context, NIP-65, and profile identity stay accessible.</span>
</div>
</div>
</div>
<div class="mt-6 max-w-xl mx-auto text-left space-y-2">
<label for="private-key-input" class="block text-sm font-semibold text-gray-700">Private Key (nsec or 64 hex)</label>
<div class="flex flex-col sm:flex-row gap-2">
<input id="private-key-input" type="password" autocomplete="off" placeholder="nsec1... or hex"
class="border border-gray-300 rounded-lg px-4 py-2 w-full text-gray-700 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button id="private-key-login"
class="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors shadow-lg">
Use Private Key

<div class="nox-auth-card space-y-5">
<div>
<p class="nox-kicker">Authentication</p>
<h3 class="nox-panel-title">Choose an entry point</h3>
<p class="nox-panel-copy">Extension sign-in is recommended. Local key mode stays on this device until logout.</p>
</div>

<div class="nox-auth-actions">
<button id="welcome-login" class="nox-primary-button py-3 px-6">
<span aria-hidden="true">🔑</span>
<span>Connect Extension</span>
</button>
<button id="welcome-global" class="nox-secondary-button py-3 px-6">
<span aria-hidden="true">🌍</span>
<span>View Global Timeline</span>
</button>
</div>
<p class="text-xs text-gray-500">Private keys are stored on this device so you stay logged in after closing the app. Log out to remove it. For better security, use a Nostr extension instead.</p>

<div class="space-y-2">
<label for="private-key-input" class="nox-field-label">Private key access</label>
<div class="flex flex-col sm:flex-row gap-2">
<input id="private-key-input" type="password" autocomplete="off" placeholder="nsec1... or 64-char hex"
class="nox-input px-4 py-3 text-sm" />
<button id="private-key-login"
class="nox-secondary-button py-3 px-5 whitespace-nowrap">
Use Private Key
</button>
</div>
<p class="nox-auth-note">Private keys are stored locally so you remain signed in after closing the app. Use an extension when possible for better isolation.</p>
</div>
</div>
</div>
</section>
`;

const welcomeLoginBtn: HTMLElement | null =
Expand All @@ -68,14 +110,15 @@ export async function showInputForm(
if (welcomeLoginBtn) {
welcomeLoginBtn.addEventListener('click', async (): Promise<void> => {
try {
if (!(window as any).nostr) {
const nostrWindow: WindowWithNostr = window as WindowWithNostr;
if (!nostrWindow.nostr) {
alert(
'No Nostr extension found!\n\nPlease install a Nostr browser extension like:\n- Alby (getalby.com)\n- nos2x\n- Flamingo\n\nThen reload this page.',
'No compatible extension found!\n\nPlease install a browser extension that exposes the nostr signing API, such as:\n- Alby (getalby.com)\n- nos2x\n- Flamingo\n\nThen reload this page.',
);
return;
}

const pubkeyHex: string = await (window as any).nostr.getPublicKey();
const pubkeyHex: string = await nostrWindow.nostr.getPublicKey();
if (!pubkeyHex) {
alert('Failed to get public key from extension.');
return;
Expand Down
2 changes: 1 addition & 1 deletion src/features/relays/relays-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function loadRelaysPage(options: RelaysPageOptions): void {
<div class="bg-slate-50 border border-slate-200 text-slate-900 rounded-lg p-3 text-xs space-y-2">
<div class="font-semibold">NIP-65 (kind 10002) Relay List</div>
<div class="text-slate-700">
You can publish your relay list to Nostr (so other clients can discover it), or import it back into this app.
You can publish your relay list to the network so other clients can discover it, or import it back into this app.
</div>
<div class="flex flex-col sm:flex-row gap-2">
<button id="nip65-import"
Expand Down
Loading
Loading