refactor: Migrate from ActivityPub federation to native Swarm network

- Remove ActivityPub infrastructure (webfinger, nodeinfo, inbox, activities, signatures)
- Implement native peer-to-peer Swarm network with gossip protocol
- Replace federated timeline with Swarm timeline aggregating posts from all nodes
- Migrate direct messaging to end-to-end encrypted Swarm Chat system
- Update user interactions (follow, like, repost) to use Swarm protocol
- Add cryptographic key management for DID-based identity system
- Remove server-bound identity model in favor of portable DID identities
- Update documentation to reflect Swarm architecture and capabilities
- Add debug utilities and chat testing tools for Swarm network
- Refactor database schema to support distributed user directory
- Update bot framework to work with Swarm network interactions
- Simplify API routes to use Swarm protocol instead of ActivityPub
- This transition enables true peer-to-peer communication, instant interactions, and encrypted messaging while maintaining sovereign identity through DIDs
This commit is contained in:
Christomatt
2026-01-27 01:27:15 +01:00
parent 6b8eeb6814
commit eb63194c56
49 changed files with 1173 additions and 3559 deletions
+87 -388
View File
@@ -2,8 +2,9 @@
import { useState, useEffect, useRef } from 'react';
import { useAuth } from '@/lib/contexts/AuthContext';
import { MessageCircle, Send, ArrowLeft } from 'lucide-react';
import { MessageCircle, Send, ArrowLeft, Search, Plus } from 'lucide-react';
import { formatFullHandle } from '@/lib/utils/handle';
import './chat.css';
interface Conversation {
id: string;
@@ -39,6 +40,7 @@ export default function ChatPage() {
const [showNewChat, setShowNewChat] = useState(false);
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -87,7 +89,6 @@ export default function ChatPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ conversationId }),
});
// Update unread count locally
setConversations(prev =>
prev.map(c => c.id === conversationId ? { ...c, unreadCount: 0 } : c)
);
@@ -115,7 +116,7 @@ export default function ChatPage() {
const data = await res.json();
setMessages(prev => [...prev, data.message]);
setNewMessage('');
loadConversations(); // Refresh to update last message
loadConversations();
}
} catch (error) {
console.error('Failed to send message:', error);
@@ -151,12 +152,18 @@ export default function ChatPage() {
}
};
const filteredConversations = conversations.filter(conv =>
conv.participant2.displayName.toLowerCase().includes(searchQuery.toLowerCase()) ||
conv.participant2.handle.toLowerCase().includes(searchQuery.toLowerCase())
);
if (!user) {
return (
<div className="chat-page">
<div style={{ padding: '48px', textAlign: 'center' }}>
<MessageCircle size={48} style={{ margin: '0 auto 16px', opacity: 0.5 }} />
<p>Sign in to use Swarm Chat</p>
<div className="chat-empty-state">
<MessageCircle size={64} />
<h2>Sign in to use Swarm Chat</h2>
<p>End-to-end encrypted messaging across the Synapsis network</p>
</div>
</div>
);
@@ -165,34 +172,43 @@ export default function ChatPage() {
return (
<div className="chat-page">
<div className="chat-container">
{/* Conversations List */}
{/* Sidebar */}
<div className={`chat-sidebar ${selectedConversation ? 'mobile-hidden' : ''}`}>
<div className="chat-sidebar-header">
<h2>Swarm Chat</h2>
<button
className="btn-primary"
onClick={() => setShowNewChat(true)}
style={{ padding: '8px 16px', fontSize: '14px' }}
>
New Chat
<h1>Messages</h1>
<button className="btn-icon" onClick={() => setShowNewChat(true)} title="New chat">
<Plus size={20} />
</button>
</div>
<div className="chat-search">
<Search size={18} />
<input
type="text"
placeholder="Search conversations..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{loading ? (
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
Loading...
<div className="chat-loading">
<div className="spinner" />
<p>Loading conversations...</p>
</div>
) : conversations.length === 0 ? (
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
<MessageCircle size={32} style={{ margin: '0 auto 12px' }} />
<p>No conversations yet</p>
<p style={{ fontSize: '14px', marginTop: '8px' }}>
Start a chat with anyone on the swarm
</p>
) : filteredConversations.length === 0 ? (
<div className="chat-empty-state">
<MessageCircle size={48} />
<h3>No conversations yet</h3>
<p>Start a chat with anyone on the swarm</p>
<button className="btn-primary" onClick={() => setShowNewChat(true)}>
<Plus size={18} />
New Chat
</button>
</div>
) : (
<div className="conversations-list">
{conversations.map((conv) => (
{filteredConversations.map((conv) => (
<button
key={conv.id}
className={`conversation-item ${selectedConversation?.id === conv.id ? 'active' : ''}`}
@@ -202,19 +218,17 @@ export default function ChatPage() {
{conv.participant2.avatarUrl ? (
<img src={conv.participant2.avatarUrl} alt={conv.participant2.displayName} />
) : (
conv.participant2.displayName.charAt(0).toUpperCase()
<span>{conv.participant2.displayName.charAt(0).toUpperCase()}</span>
)}
</div>
<div className="conversation-info">
<div className="conversation-name">
{conv.participant2.displayName}
<div className="conversation-content">
<div className="conversation-header">
<span className="conversation-name">{conv.participant2.displayName}</span>
{conv.unreadCount > 0 && (
<span className="unread-badge">{conv.unreadCount}</span>
)}
</div>
<div className="conversation-handle">
{formatFullHandle(conv.participant2.handle)}
</div>
<div className="conversation-handle">{formatFullHandle(conv.participant2.handle)}</div>
<div className="conversation-preview">{conv.lastMessagePreview}</div>
</div>
</button>
@@ -223,60 +237,48 @@ export default function ChatPage() {
)}
</div>
{/* Messages View */}
{/* Main Chat Area */}
<div className={`chat-main ${!selectedConversation ? 'mobile-hidden' : ''}`}>
{selectedConversation ? (
<>
<div className="chat-header">
<button
className="back-button"
onClick={() => setSelectedConversation(null)}
>
<button className="btn-icon back-button" onClick={() => setSelectedConversation(null)}>
<ArrowLeft size={20} />
</button>
<div className="chat-header-avatar">
{selectedConversation.participant2.avatarUrl ? (
<img src={selectedConversation.participant2.avatarUrl} alt="" />
) : (
selectedConversation.participant2.displayName.charAt(0).toUpperCase()
<span>{selectedConversation.participant2.displayName.charAt(0).toUpperCase()}</span>
)}
</div>
<div className="chat-header-info">
<div className="chat-header-name">
{selectedConversation.participant2.displayName}
</div>
<div className="chat-header-handle">
{formatFullHandle(selectedConversation.participant2.handle)}
</div>
<h2>{selectedConversation.participant2.displayName}</h2>
<p>{formatFullHandle(selectedConversation.participant2.handle)}</p>
</div>
</div>
<div className="chat-messages">
{messages.map((msg) => (
<div
key={msg.id}
className={`message ${msg.isSentByMe ? 'sent' : 'received'}`}
>
<div key={msg.id} className={`message ${msg.isSentByMe ? 'sent' : 'received'}`}>
{!msg.isSentByMe && (
<div className="message-avatar">
{msg.senderAvatarUrl ? (
<img src={msg.senderAvatarUrl} alt="" />
) : (
(msg.senderDisplayName || msg.senderHandle).charAt(0).toUpperCase()
<span>{(msg.senderDisplayName || msg.senderHandle).charAt(0).toUpperCase()}</span>
)}
</div>
)}
<div className="message-content">
<div className="message-bubble">
{/* Note: In production, decrypt client-side */}
<div style={{ opacity: 0.5, fontSize: '12px' }}>
[Encrypted: {msg.encryptedContent.substring(0, 20)}...]
</div>
<div className="encrypted-indicator">🔒 Encrypted</div>
<div className="encrypted-preview">{msg.encryptedContent.substring(0, 40)}...</div>
</div>
<div className="message-meta">
{new Date(msg.createdAt).toLocaleTimeString()}
<span>{new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
{msg.isSentByMe && (
<span style={{ marginLeft: '8px', opacity: 0.7 }}>
<span className="delivery-status">
{msg.readAt ? '✓✓' : msg.deliveredAt ? '✓' : '○'}
</span>
)}
@@ -295,350 +297,47 @@ export default function ChatPage() {
onChange={(e) => setNewMessage(e.target.value)}
disabled={sending}
/>
<button type="submit" disabled={sending || !newMessage.trim()}>
<button type="submit" className="btn-icon send-button" disabled={sending || !newMessage.trim()}>
<Send size={20} />
</button>
</form>
</>
) : (
<div className="chat-empty">
<MessageCircle size={64} style={{ opacity: 0.3, marginBottom: '16px' }} />
<p>Select a conversation to start chatting</p>
<div className="chat-empty-state">
<MessageCircle size={64} />
<h2>Select a conversation</h2>
<p>Choose a conversation from the sidebar to start chatting</p>
</div>
)}
</div>
{/* New Chat Modal */}
{showNewChat && (
<div className="modal-overlay" onClick={() => setShowNewChat(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h3>Start New Chat</h3>
<form onSubmit={startNewChat}>
<input
type="text"
placeholder="Enter handle (e.g., user@node.domain)"
value={newChatHandle}
onChange={(e) => setNewChatHandle(e.target.value)}
autoFocus
/>
<div className="modal-actions">
<button
type="button"
className="btn-secondary"
onClick={() => setShowNewChat(false)}
>
Cancel
</button>
<button
type="submit"
className="btn-primary"
disabled={sending || !newChatHandle.trim()}
>
Start Chat
</button>
</div>
</form>
</div>
</div>
)}
</div>
<style jsx>{`
.chat-page {
max-width: 1200px;
margin: 0 auto;
height: calc(100vh - 60px);
}
.chat-container {
display: grid;
grid-template-columns: 350px 1fr;
height: 100%;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
background: var(--background);
}
.chat-sidebar {
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.chat-sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-sidebar-header h2 {
margin: 0;
font-size: 18px;
}
.conversations-list {
overflow-y: auto;
flex: 1;
}
.conversation-item {
display: flex;
gap: 12px;
padding: 12px 16px;
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
border-bottom: 1px solid var(--border);
transition: background 0.2s;
}
.conversation-item:hover {
background: var(--background-secondary);
}
.conversation-item.active {
background: var(--accent-muted);
}
.conversation-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--accent);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.conversation-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.conversation-info {
flex: 1;
min-width: 0;
}
.conversation-name {
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.unread-badge {
background: var(--accent);
color: white;
font-size: 11px;
padding: 2px 6px;
border-radius: 10px;
font-weight: 600;
}
.conversation-handle {
font-size: 13px;
color: var(--foreground-secondary);
}
.conversation-preview {
font-size: 14px;
color: var(--foreground-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 4px;
}
.chat-main {
display: flex;
flex-direction: column;
}
.chat-header {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
}
.back-button {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 8px;
color: var(--foreground);
}
.chat-header-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--accent);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.chat-header-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.chat-header-info {
flex: 1;
}
.chat-header-name {
font-weight: 600;
}
.chat-header-handle {
font-size: 13px;
color: var(--foreground-secondary);
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
gap: 8px;
}
.message.sent {
flex-direction: row-reverse;
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--accent);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.message-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.message-content {
max-width: 70%;
}
.message.sent .message-content {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.message-bubble {
padding: 12px 16px;
border-radius: 16px;
background: var(--background-secondary);
word-wrap: break-word;
}
.message.sent .message-bubble {
background: var(--accent);
color: white;
}
.message-meta {
font-size: 11px;
color: var(--foreground-tertiary);
margin-top: 4px;
padding: 0 8px;
}
.chat-input {
padding: 16px;
border-top: 1px solid var(--border);
display: flex;
gap: 8px;
}
.chat-input input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--border);
border-radius: 24px;
background: var(--background-secondary);
color: var(--foreground);
}
.chat-input button {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--accent);
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.chat-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.chat-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--foreground-tertiary);
}
@media (max-width: 768px) {
.chat-container {
grid-template-columns: 1fr;
}
.mobile-hidden {
display: none;
}
.back-button {
display: block;
}
}
`}</style>
{/* New Chat Modal */}
{showNewChat && (
<div className="modal-overlay" onClick={() => setShowNewChat(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>Start New Chat</h2>
<p>Enter the handle of the person you want to message</p>
<form onSubmit={startNewChat}>
<input
type="text"
placeholder="user@node.domain or localuser"
value={newChatHandle}
onChange={(e) => setNewChatHandle(e.target.value)}
autoFocus
/>
<div className="modal-actions">
<button type="button" className="btn-secondary" onClick={() => setShowNewChat(false)}>
Cancel
</button>
<button type="submit" className="btn-primary" disabled={sending || !newChatHandle.trim()}>
Start Chat
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}