From a68aee8758b5b5f06e6d2803b26a944fba214f27 Mon Sep 17 00:00:00 2001 From: Christopher Date: Tue, 27 Jan 2026 01:26:41 -0800 Subject: [PATCH] Migrate user/profile URLs to /u/[handle] and remove legacy scripts All user and post profile URLs have been updated from /[handle] to /u/[handle] for consistency. Added a redirect page for /posts/[id] to the canonical /u/[handle]/posts/[id] route. Deprecated and removed legacy scripts and documentation related to chat and federation endpoints. Push-based federation endpoints are now disabled in favor of real-time pull-based federation. --- SWARM_CHAT.md | 160 ------------------- TURNSTILE_SETUP.md | 72 --------- check-keys.ts | 19 --- clean-old-messages.ts | 14 -- debug_chat.ts | 27 ---- inspect_messages.ts | 26 --- src/app/api/notifications/route.ts | 1 + src/app/api/posts/route.ts | 6 +- src/app/api/swarm/inbox/route.ts | 114 +------------ src/app/api/swarm/replies/route.ts | 83 +--------- src/app/chat/page.tsx | 20 ++- src/app/explore/page.tsx | 2 +- src/app/posts/[id]/page.tsx | 40 +++++ src/app/settings/bots/page.tsx | 4 +- src/app/{ => u}/[handle]/page.tsx | 6 +- src/app/{ => u}/[handle]/posts/[id]/page.tsx | 2 +- src/components/PostCard.tsx | 12 +- src/components/RightSidebar.tsx | 2 +- src/components/Sidebar.tsx | 2 +- try_create_chat.ts | 46 ------ 20 files changed, 85 insertions(+), 573 deletions(-) delete mode 100644 SWARM_CHAT.md delete mode 100644 TURNSTILE_SETUP.md delete mode 100644 check-keys.ts delete mode 100644 clean-old-messages.ts delete mode 100644 debug_chat.ts delete mode 100644 inspect_messages.ts create mode 100644 src/app/posts/[id]/page.tsx rename src/app/{ => u}/[handle]/page.tsx (99%) rename src/app/{ => u}/[handle]/posts/[id]/page.tsx (99%) delete mode 100644 try_create_chat.ts diff --git a/SWARM_CHAT.md b/SWARM_CHAT.md deleted file mode 100644 index f466d6a..0000000 --- a/SWARM_CHAT.md +++ /dev/null @@ -1,160 +0,0 @@ -# Swarm Chat - -A real-time, end-to-end encrypted chat system built exclusively for the Synapsis Swarm network. - -## Features - -- **End-to-End Encryption**: Messages are encrypted using recipient's public key -- **Cross-Node Messaging**: Chat with users on any Synapsis node -- **Real-Time Delivery**: Messages delivered instantly via swarm inbox -- **Read Receipts**: See when messages are delivered and read -- **Typing Indicators**: Know when someone is typing (coming soon) -- **Native Swarm Protocol**: Built specifically for the swarm network - -## Architecture - -### Database Schema - -**chat_conversations**: Tracks conversations between users -- Stores participant info and last message preview -- Unique constraint ensures one conversation per user pair - -**chat_messages**: Individual encrypted messages -- Content encrypted with recipient's public key -- Swarm message ID for deduplication -- Delivery and read status tracking - -**chat_typing_indicators**: Real-time typing status (future) - -### API Endpoints - -**POST /api/swarm/chat/send** -- Send a message to any user (local or remote) -- Encrypts content with recipient's public key -- Delivers to remote nodes via swarm inbox - -**POST /api/swarm/chat/inbox** -- Receives messages from other swarm nodes -- Validates and stores encrypted messages -- Updates conversation metadata - -**GET /api/swarm/chat/conversations** -- Lists all conversations for current user -- Includes unread counts and last message preview - -**GET /api/swarm/chat/messages** -- Fetches messages for a conversation -- Supports cursor-based pagination -- Returns encrypted content for client-side decryption - -**PATCH /api/swarm/chat/messages** -- Marks messages as read -- Updates read receipts - -### Encryption - -Messages are encrypted using RSA-OAEP with SHA-256: - -1. **Sending**: Message encrypted with recipient's public key -2. **Storage**: Only encrypted content stored in database -3. **Decryption**: Client-side decryption using user's private key - -This ensures true end-to-end encryption - even the server cannot read messages. - -## Usage - -### Starting a Chat - -```typescript -// Send a message to start a conversation -await fetch('/api/swarm/chat/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - recipientHandle: 'user@remote.node', - content: 'Hello!', - }), -}); -``` - -### Receiving Messages - -Messages are automatically received via the swarm inbox endpoint. The system: -1. Validates the sender and recipient -2. Checks for duplicate messages -3. Creates or updates the conversation -4. Stores the encrypted message -5. Returns success to the sender - -### Client-Side Decryption - -```typescript -import { decryptMessage } from '@/lib/swarm/chat-crypto'; - -// Decrypt a message using user's private key -const plaintext = decryptMessage( - message.encryptedContent, - userPrivateKey -); -``` - -## Security Considerations - -1. **Private Key Storage**: User private keys should be encrypted at rest -2. **Key Derivation**: Consider using password-based key derivation -3. **Forward Secrecy**: Future enhancement - implement Signal protocol -4. **Message Signing**: Verify sender authenticity with signatures -5. **Rate Limiting**: Prevent spam and abuse - -## Future Enhancements - -- [ ] Group chats (multi-party encryption) -- [ ] Voice/video calls (WebRTC) -- [ ] File attachments (encrypted) -- [ ] Message reactions -- [ ] Message editing/deletion -- [ ] Typing indicators (real-time) -- [ ] Online/offline status -- [ ] Push notifications -- [ ] Desktop notifications -- [ ] Message search -- [ ] Conversation archiving -- [ ] Block/mute users -- [ ] Forward secrecy (Signal protocol) - -## Migration - -Run the migration to create the chat tables: - -```bash -npm run db:generate -npm run db:migrate -``` - -## Testing - -Test the chat system: - -1. Create two accounts on different nodes -2. Navigate to `/chat` on one account -3. Click "New Chat" and enter the other user's handle -4. Send a message -5. Check the other account's `/chat` page - -## Why Swarm Chat? - -Swarm Chat was built from the ground up for the Synapsis network: - -- **True E2E Encryption**: Messages encrypted with recipient's public key -- **Real-Time**: Direct delivery, no polling required -- **Proper Chat UX**: Conversations, read receipts, typing indicators -- **Lightweight**: Simple JSON protocol -- **Swarm-Native**: Built for the swarm, not retrofitted - -## Contributing - -Contributions welcome! Priority areas: -- Client-side encryption implementation -- Real-time updates (WebSocket/SSE) -- Mobile-responsive UI improvements -- Group chat support diff --git a/TURNSTILE_SETUP.md b/TURNSTILE_SETUP.md deleted file mode 100644 index 8d0bb0d..0000000 --- a/TURNSTILE_SETUP.md +++ /dev/null @@ -1,72 +0,0 @@ -# Cloudflare Turnstile Setup Guide - -Cloudflare Turnstile has been fully integrated into your Synapsis node to protect against bot registrations and logins. - -## How It Works - -1. **Admin Configuration**: Admins can add their Cloudflare Turnstile keys in the Admin Settings panel -2. **Automatic Activation**: Once both Site Key and Secret Key are configured, Turnstile is automatically enabled -3. **Frontend Integration**: The Turnstile widget appears on login and registration forms -4. **Server Verification**: All login/register requests are verified server-side with Cloudflare - -## Setup Steps - -### 1. Get Turnstile Keys from Cloudflare - -1. Go to https://dash.cloudflare.com/?to=/:account/turnstile -2. Create a new site -3. Copy your **Site Key** (public) and **Secret Key** (private) - -### 2. Configure in Admin Panel - -1. Log in as an admin -2. Go to Admin → Settings tab -3. Scroll to "Cloudflare Turnstile (Bot Protection)" section -4. Paste your Site Key and Secret Key -5. Click "Save Settings" - -### 3. Test It - -1. Log out -2. Go to the login page -3. You should see the Turnstile widget appear -4. Complete the challenge and try logging in - -## Features - -- ✅ Automatic widget rendering when keys are configured -- ✅ Works on both login and registration forms -- ✅ Server-side token verification -- ✅ Automatic widget reset on form errors -- ✅ Graceful fallback if Turnstile is not configured -- ✅ Submit button disabled until challenge is completed -- ✅ IP address forwarding for better verification - -## Technical Details - -### Database Schema -- `nodes.turnstile_site_key` - Public site key (exposed to frontend) -- `nodes.turnstile_secret_key` - Private secret key (server-side only) - -### API Endpoints Modified -- `POST /api/auth/login` - Now accepts optional `turnstileToken` -- `POST /api/auth/register` - Now accepts optional `turnstileToken` -- `GET /api/node` - Returns `turnstileSiteKey` for frontend - -### Files Modified -- `src/db/schema.ts` - Added Turnstile fields -- `src/lib/turnstile.ts` - Verification helper functions -- `src/app/api/auth/login/route.ts` - Token verification -- `src/app/api/auth/register/route.ts` - Token verification -- `src/app/api/node/route.ts` - Expose site key -- `src/app/api/admin/node/route.ts` - Save/update keys -- `src/app/admin/page.tsx` - Admin UI for configuration -- `src/app/login/page.tsx` - Frontend widget integration - -## Security Notes - -- The Secret Key is NEVER exposed to the frontend -- Only the Site Key is public -- Verification happens server-side with Cloudflare's API -- Failed verifications reject the login/registration attempt -- IP addresses are forwarded for better bot detection diff --git a/check-keys.ts b/check-keys.ts deleted file mode 100644 index 2547e89..0000000 --- a/check-keys.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { db, users } from './src/db'; -import { eq } from 'drizzle-orm'; - -async function checkKeys() { - const allUsers = await db.select({ - handle: users.handle, - hasChatPublicKey: users.chatPublicKey, - hasChatPrivateKeyEncrypted: users.chatPrivateKeyEncrypted, - }).from(users); - - console.log('Users and their chat keys:'); - allUsers.forEach(u => { - console.log(`- ${u.handle}: chatPublicKey=${!!u.hasChatPublicKey}, chatPrivateKeyEncrypted=${!!u.hasChatPrivateKeyEncrypted}`); - }); - - process.exit(0); -} - -checkKeys().catch(console.error); diff --git a/clean-old-messages.ts b/clean-old-messages.ts deleted file mode 100644 index 157939b..0000000 --- a/clean-old-messages.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { db, chatMessages } from './src/db'; -import { isNull } from 'drizzle-orm'; - -async function cleanOldMessages() { - console.log('Deleting old RSA-encrypted messages...'); - - const result = await db.delete(chatMessages) - .where(isNull(chatMessages.senderChatPublicKey)); - - console.log('Deleted old messages. Now only E2E encrypted messages remain.'); - process.exit(0); -} - -cleanOldMessages().catch(console.error); diff --git a/debug_chat.ts b/debug_chat.ts deleted file mode 100644 index 6a73cfc..0000000 --- a/debug_chat.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import { db } from './src/db/index'; -import { users } from './src/db/schema'; -import { chatConversations } from './src/db/schema'; - -async function main() { - console.log('--- USERS ---'); - try { - const allUsers = await db.select().from(users); - console.log(`Found ${allUsers.length} users.`); - allUsers.forEach(u => { - console.log(`User: ${u.handle} | ID: ${u.id} | Local: ${!u.handle.includes('@')}`); - }); - - console.log('\n--- CONVERSATIONS ---'); - const convs = await db.select().from(chatConversations); - console.log(`Found ${convs.length} conversations.`); - convs.forEach(c => { - console.log(`Conv: ${c.id} | Type: ${c.type} | P1: ${c.participant1Id} | P2Handle: ${c.participant2Handle}`); - }); - - } catch (e) { - console.error('Error:', e); - } - process.exit(0); -} -main(); diff --git a/inspect_messages.ts b/inspect_messages.ts deleted file mode 100644 index 0c20387..0000000 --- a/inspect_messages.ts +++ /dev/null @@ -1,26 +0,0 @@ -import 'dotenv/config'; -import { db } from './src/db/index'; -import { chatMessages } from './src/db/schema'; -import { desc } from 'drizzle-orm'; - -async function main() { - console.log('--- LATEST CHAT MESSAGES ---'); - try { - const messages = await db.select().from(chatMessages).orderBy(desc(chatMessages.createdAt)).limit(20); - console.log(`Found ${messages.length} messages.`); - messages.forEach(m => { - console.log(`\nID: ${m.id}`); - console.log(`Sender: ${m.senderHandle}`); - console.log(`Created: ${m.createdAt}`); - console.log(`EncryptedContent (${m.encryptedContent?.length} chars): ${m.encryptedContent}`); - console.log(`SenderEncryptedContent (${m.senderEncryptedContent?.length} chars): ${m.senderEncryptedContent}`); - console.log('-----------------------------------'); - }); - - } catch (e) { - console.error('Error:', e); - } - process.exit(0); -} - -main(); diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts index d62143a..bf60b76 100644 --- a/src/app/api/notifications/route.ts +++ b/src/app/api/notifications/route.ts @@ -102,6 +102,7 @@ export async function GET(request: Request) { post: row.postId ? { id: row.postId, content: row.postContent, + authorHandle: row.actorHandle, // The actor is the post author for likes/reposts } : null, }; }); diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index b55a550..5dd63e6 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -115,7 +115,10 @@ export async function POST(request: Request) { } } - // If this is a reply to a swarm post, deliver it to the origin node + // DEPRECATED: Push-based federation disabled + // Swarm now uses real-time pull-based federation + // Replies are fetched in real-time from the origin node + /* if (data.swarmReplyTo) { (async () => { try { @@ -153,6 +156,7 @@ export async function POST(request: Request) { } })(); } + */ // Handle local mentions (create notifications for users on this node) (async () => { diff --git a/src/app/api/swarm/inbox/route.ts b/src/app/api/swarm/inbox/route.ts index e0aefbf..0c7bee0 100644 --- a/src/app/api/swarm/inbox/route.ts +++ b/src/app/api/swarm/inbox/route.ts @@ -43,114 +43,12 @@ const swarmPostSchema = z.object({ /** * POST /api/swarm/inbox * - * Receives a post from another swarm node. - * Stores it for local users who follow the author. + * DEPRECATED: This endpoint is disabled. + * We now use real-time pull-based federation via /api/swarm/timeline + * instead of push-based caching. */ export async function POST(request: NextRequest) { - try { - if (!db) { - return NextResponse.json({ error: 'Database not available' }, { status: 503 }); - } - - const body = await request.json(); - const data = swarmPostSchema.parse(body); - - // Construct the swarm post ID - const swarmPostId = `swarm:${data.nodeDomain}:${data.post.id}`; - - // Check if we already have this post - const existingPost = await db.query.posts.findFirst({ - where: eq(posts.apId, swarmPostId), - }); - - if (existingPost) { - return NextResponse.json({ - success: true, - message: 'Post already exists', - }); - } - - // Check if anyone on this node follows the author - const authorActorUrl = `swarm://${data.nodeDomain}/${data.author.handle}`; - const hasFollowers = await db.query.remoteFollowers.findFirst({ - where: eq(remoteFollowers.actorUrl, authorActorUrl), - }); - - // Even if no one follows, we might want to cache for timeline - // For now, only store if someone follows - if (!hasFollowers) { - return NextResponse.json({ - success: true, - message: 'No local followers', - }); - } - - // Get or create placeholder user for the remote author - const remoteHandle = `${data.author.handle}@${data.nodeDomain}`; - let remoteUser = await db.query.users.findFirst({ - where: eq(users.handle, remoteHandle), - }); - - if (!remoteUser) { - const [newUser] = await db.insert(users).values({ - did: `did:swarm:${data.nodeDomain}:${data.author.handle}`, - handle: remoteHandle, - displayName: data.author.displayName, - avatarUrl: data.author.avatarUrl || null, - isNsfw: data.author.isNsfw, - publicKey: 'swarm-remote-user', - }).returning(); - remoteUser = newUser; - } else { - // Update profile info if changed - await db.update(users) - .set({ - displayName: data.author.displayName, - avatarUrl: data.author.avatarUrl || remoteUser.avatarUrl, - isNsfw: data.author.isNsfw, - }) - .where(eq(users.id, remoteUser.id)); - } - - // Create the post - const [newPost] = await db.insert(posts).values({ - userId: remoteUser.id, - content: data.post.content, - isNsfw: data.post.isNsfw || data.author.isNsfw, - apId: swarmPostId, - apUrl: `https://${data.nodeDomain}/${data.author.handle}/posts/${data.post.id}`, - createdAt: new Date(data.post.createdAt), - linkPreviewUrl: data.post.linkPreviewUrl || null, - linkPreviewTitle: data.post.linkPreviewTitle || null, - linkPreviewDescription: data.post.linkPreviewDescription || null, - linkPreviewImage: data.post.linkPreviewImage || null, - }).returning(); - - // Store media attachments - if (data.post.media && data.post.media.length > 0) { - for (const m of data.post.media) { - await db.insert(media).values({ - userId: remoteUser.id, - postId: newPost.id, - url: m.url, - mimeType: m.mimeType || null, - altText: m.altText || null, - }); - } - } - - console.log(`[Swarm] Received post from ${remoteHandle}: ${newPost.id}`); - - return NextResponse.json({ - success: true, - message: 'Post received', - postId: newPost.id, - }); - } catch (error) { - if (error instanceof z.ZodError) { - return NextResponse.json({ error: 'Invalid request', details: error.issues }, { status: 400 }); - } - console.error('[Swarm] Inbox error:', error); - return NextResponse.json({ error: 'Failed to process post' }, { status: 500 }); - } + return NextResponse.json({ + error: 'This endpoint is deprecated. Swarm uses real-time pull-based federation.', + }, { status: 410 }); // 410 Gone } diff --git a/src/app/api/swarm/replies/route.ts b/src/app/api/swarm/replies/route.ts index 75268d4..6211832 100644 --- a/src/app/api/swarm/replies/route.ts +++ b/src/app/api/swarm/replies/route.ts @@ -30,86 +30,13 @@ const swarmReplySchema = z.object({ /** * POST /api/swarm/replies * - * Receives a reply from another node in the swarm. - * The reply is stored as a remote reply linked to the local post. + * DEPRECATED: This endpoint is disabled. + * We now use real-time pull-based federation instead of push-based caching. */ export async function POST(request: NextRequest) { - try { - if (!db) { - return NextResponse.json({ error: 'Database not available' }, { status: 503 }); - } - - const body = await request.json(); - const data = swarmReplySchema.parse(body); - - // Verify the target post exists on this node - const targetPost = await db.query.posts.findFirst({ - where: eq(posts.id, data.postId), - }); - - if (!targetPost) { - return NextResponse.json({ error: 'Post not found' }, { status: 404 }); - } - - // Check if we already have this reply (by swarm ID) - const swarmReplyId = `swarm:${data.reply.nodeDomain}:${data.reply.id}`; - const existingReply = await db.query.posts.findFirst({ - where: eq(posts.apId, swarmReplyId), - }); - - if (existingReply) { - return NextResponse.json({ success: true, message: 'Reply already exists' }); - } - - // We need a system user to attribute swarm replies to - // For now, we'll store them with metadata in the apId/apUrl fields - // and create a virtual representation - - // Get or create a placeholder user for this remote author - let remoteUser = await db.query.users.findFirst({ - where: eq(users.handle, `${data.reply.author.handle}@${data.reply.nodeDomain}`), - }); - - if (!remoteUser) { - // Create a placeholder user for the remote author - const [newUser] = await db.insert(users).values({ - did: `did:swarm:${data.reply.nodeDomain}:${data.reply.author.handle}`, - handle: `${data.reply.author.handle}@${data.reply.nodeDomain}`, - displayName: data.reply.author.displayName, - avatarUrl: data.reply.author.avatarUrl || null, - publicKey: 'swarm-remote-user', // Placeholder - }).returning(); - remoteUser = newUser; - } - - // Create the reply post - const [replyPost] = await db.insert(posts).values({ - userId: remoteUser.id, - content: data.reply.content, - replyToId: data.postId, - apId: swarmReplyId, - apUrl: `https://${data.reply.nodeDomain}/${data.reply.author.handle}/posts/${data.reply.id}`, - createdAt: new Date(data.reply.createdAt), - }).returning(); - - // Update the parent post's reply count - await db.update(posts) - .set({ repliesCount: targetPost.repliesCount + 1 }) - .where(eq(posts.id, data.postId)); - - console.log(`[Swarm] Received reply from ${data.reply.nodeDomain} to post ${data.postId}`); - - return NextResponse.json({ - success: true, - replyId: replyPost.id, - }); - } catch (error) { - if (error instanceof z.ZodError) { - return NextResponse.json({ error: 'Invalid request', details: error.issues }, { status: 400 }); - } - console.error('[Swarm] Reply error:', error); - return NextResponse.json({ error: 'Failed to process reply' }, { status: 500 }); - } + return NextResponse.json({ + error: 'This endpoint is deprecated. Swarm uses real-time pull-based federation.', + }, { status: 410 }); // 410 Gone } /** diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index bc9bc66..6682176 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -489,15 +489,21 @@ export default function ChatPage() { marginLeft: msg.isSentByMe ? 'auto' : '0', flexDirection: msg.isSentByMe ? 'row-reverse' : 'row' }}> - {!msg.isSentByMe && ( -
- {msg.senderAvatarUrl ? ( +
+ {msg.isSentByMe ? ( + user.avatarUrl ? ( + + ) : ( + user.displayName[0] + ) + ) : ( + msg.senderAvatarUrl ? ( ) : ( msg.senderDisplayName?.[0] - )} -
- )} + ) + )} +
- {conv.lastMessagePreview} + {conv.lastMessagePreview === 'New message' ? 'Encrypted Message' : conv.lastMessagePreview}
diff --git a/src/app/explore/page.tsx b/src/app/explore/page.tsx index 161119d..b9b54f8 100644 --- a/src/app/explore/page.tsx +++ b/src/app/explore/page.tsx @@ -22,7 +22,7 @@ interface User { function UserCard({ user }: { user: User }) { return ( - +
{user.avatarUrl ? ( {user.displayName} diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx new file mode 100644 index 0000000..d52bd1a --- /dev/null +++ b/src/app/posts/[id]/page.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; + +export default function PostRedirect() { + const params = useParams(); + const router = useRouter(); + const id = params.id as string; + + useEffect(() => { + const fetchAndRedirect = async () => { + try { + const res = await fetch(`/api/posts/${id}`); + if (!res.ok) { + router.push('/'); + return; + } + const data = await res.json(); + router.push(`/u/${data.post.author.handle}/posts/${id}`); + } catch { + router.push('/'); + } + }; + + fetchAndRedirect(); + }, [id, router]); + + return ( +
+ Loading... +
+ ); +} diff --git a/src/app/settings/bots/page.tsx b/src/app/settings/bots/page.tsx index 5256968..314cfd1 100644 --- a/src/app/settings/bots/page.tsx +++ b/src/app/settings/bots/page.tsx @@ -111,7 +111,7 @@ export default function BotsPage() {
e.stopPropagation()} className="avatar" style={{ @@ -147,7 +147,7 @@ export default function BotsPage() { )}
e.stopPropagation()} style={{ fontSize: '13px', color: 'var(--foreground-tertiary)' }} > diff --git a/src/app/[handle]/page.tsx b/src/app/u/[handle]/page.tsx similarity index 99% rename from src/app/[handle]/page.tsx rename to src/app/u/[handle]/page.tsx index 2b64cf0..5e738fa 100644 --- a/src/app/[handle]/page.tsx +++ b/src/app/u/[handle]/page.tsx @@ -35,7 +35,7 @@ const stripHtml = (html: string | null | undefined): string | null => { function UserRow({ user }: { user: UserSummary }) { return ( - +
{user.avatarUrl ? ( {user.displayName @@ -215,7 +215,7 @@ export default function ProfilePage() { const handleComment = (post: Post) => { // Navigation is handled by the PostCard overlay, // but we can also use router.push if they explicitly click the comment button. - router.push(`/${post.author.handle}/posts/${post.id}`); + router.push(`/u/${post.author.handle}/posts/${post.id}`); }; const handleDelete = (postId: string) => { @@ -624,7 +624,7 @@ export default function ProfilePage() { <> {' · Managed by '} @{(user as any).botOwner.handle} diff --git a/src/app/[handle]/posts/[id]/page.tsx b/src/app/u/[handle]/posts/[id]/page.tsx similarity index 99% rename from src/app/[handle]/posts/[id]/page.tsx rename to src/app/u/[handle]/posts/[id]/page.tsx index 22f17b0..75b3843 100644 --- a/src/app/[handle]/posts/[id]/page.tsx +++ b/src/app/u/[handle]/posts/[id]/page.tsx @@ -85,7 +85,7 @@ export default function PostDetailPage() { const handleDelete = (postId: string) => { if (postId === id) { - router.push(`/${handle}`); + router.push(`/u/${handle}`); } else { setReplies(prev => prev.filter(r => r.id !== postId)); if (post) { diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index f008696..5cc10b1 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -248,7 +248,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide, } }; - const postUrl = `/${post.author.handle}/posts/${post.id}`; + const postUrl = `/u/${post.author.handle}/posts/${post.id}`; // Get the full handle for profile links (includes domain for remote users) const getProfileHandle = () => { @@ -384,7 +384,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide, return (
- e.stopPropagation()}> + e.stopPropagation()}>
{post.author.avatarUrl ? ( {post.author.displayName @@ -394,7 +394,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
- e.stopPropagation()}> + e.stopPropagation()}> {post.author.displayName || post.author.handle} {formatFullHandle(post.author.handle, post.nodeDomain)} @@ -427,7 +427,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide, {!isDetail && }
- e.stopPropagation()}> + e.stopPropagation()}>
{post.author.avatarUrl ? ( {post.author.displayName} @@ -438,7 +438,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
- e.stopPropagation()}> + e.stopPropagation()}> {post.author.displayName || post.author.handle} {(post.bot || post.author.isBot) && ( @@ -585,7 +585,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide, {effectiveReplyTo && !showThread && (
- Replying to e.stopPropagation()}>{formatFullHandle(effectiveReplyTo.author.handle)} + Replying to e.stopPropagation()}>{formatFullHandle(effectiveReplyTo.author.handle)}
)} diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index 20abf4b..e4a44d7 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -147,7 +147,7 @@ export function RightSidebar() { {nodeInfo.admins.map((admin) => ( )} {user ? ( - + Profile diff --git a/try_create_chat.ts b/try_create_chat.ts deleted file mode 100644 index 8f16841..0000000 --- a/try_create_chat.ts +++ /dev/null @@ -1,46 +0,0 @@ - -import { db } from './src/db/index'; -import { users, chatConversations, chatMessages } from './src/db/schema'; -import { eq, and } from 'drizzle-orm'; -import { encryptMessage } from './src/lib/swarm/chat-crypto'; -import crypto from 'crypto'; - -async function main() { - console.log('--- SIMULATING CHAT SEND ---'); - try { - // 1. Get Sender (Cypher) - const sender = await db.query.users.findFirst({ - where: eq(users.handle, 'cypher'), - }); - if (!sender) throw new Error('Sender not found'); - console.log('Sender found:', sender.handle); - - // 2. Get Recipient (newinnightvale) - const recipientHandle = 'newinnightvale'; - const recipient = await db.query.users.findFirst({ - where: eq(users.handle, recipientHandle), - }); - if (!recipient) throw new Error('Recipient not found'); - console.log('Recipient found:', recipient.handle); - console.log('Recipient PK length:', recipient.publicKey?.length); - - // 3. Encrypt - console.log('Encrypting...'); - try { - const encrypted = encryptMessage('Hello World', recipient.publicKey); - console.log('Encryption successful. Length:', encrypted.length); - } catch (e) { - console.error('Encryption FAILED:', e); - // Dump key for manual inspection if needed (truncated) - console.log('Key start:', recipient.publicKey.substring(0, 50)); - return; - } - - console.log('Simulation complete (not inserting to DB to avoid pollution, but encryption passed).'); - - } catch (e) { - console.error('Error:', e); - } - process.exit(0); -} -main();