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:
+87
-388
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user