feat(swarm): Implement decentralized node discovery and content federation system
- Add swarm node registry with discovery, gossip, and timeline synchronization - Implement database schema for swarm nodes, seeds, and sync logs with proper indexing - Create swarm API endpoints for node discovery, gossip protocol, timeline sharing, and announcements - Add content moderation features with NSFW flagging and muted nodes support - Implement user settings pages for content filtering and node moderation - Add background scheduler for automated swarm synchronization and node health checks - Create .well-known endpoint for swarm protocol discovery - Remove legacy bot-cron.ts in favor of integrated scheduler system - Add user account NSFW preferences and age verification tracking - Update database schema with swarm-related tables and user moderation fields - Enhance post and user components to support federated content display - Add instrumentation for monitoring swarm operations and node health
This commit is contained in:
@@ -7,7 +7,7 @@ import { ArrowLeftIcon, CalendarIcon } from '@/components/Icons';
|
||||
import { PostCard } from '@/components/PostCard';
|
||||
import { User, Post } from '@/lib/types';
|
||||
import AutoTextarea from '@/components/AutoTextarea';
|
||||
import { Rocket } from 'lucide-react';
|
||||
import { Rocket, MoreHorizontal } from 'lucide-react';
|
||||
import { formatFullHandle } from '@/lib/utils/handle';
|
||||
import { Bot } from 'lucide-react';
|
||||
|
||||
@@ -102,6 +102,8 @@ export default function ProfilePage() {
|
||||
});
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isBlocked, setIsBlocked] = useState(false);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditing(false);
|
||||
@@ -173,6 +175,7 @@ export default function ProfilePage() {
|
||||
useEffect(() => {
|
||||
if (!currentUser || !user || currentUser.handle === user.handle) {
|
||||
setIsFollowing(false);
|
||||
setIsBlocked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,6 +183,11 @@ export default function ProfilePage() {
|
||||
.then(res => res.json())
|
||||
.then(data => setIsFollowing(!!data.following))
|
||||
.catch(() => setIsFollowing(false));
|
||||
|
||||
fetch(`/api/users/${handle}/block`)
|
||||
.then(res => res.json())
|
||||
.then(data => setIsBlocked(!!data.blocked))
|
||||
.catch(() => setIsBlocked(false));
|
||||
}, [currentUser, user, handle]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -226,6 +234,22 @@ export default function ProfilePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlock = async () => {
|
||||
if (!currentUser) return;
|
||||
|
||||
const method = isBlocked ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/users/${handle}/block`, { method });
|
||||
|
||||
if (res.ok) {
|
||||
setIsBlocked(!isBlocked);
|
||||
if (!isBlocked) {
|
||||
// If blocking, also unfollow
|
||||
setIsFollowing(false);
|
||||
}
|
||||
setShowMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
if (!isOwnProfile) return;
|
||||
setIsSaving(true);
|
||||
@@ -377,14 +401,67 @@ export default function ProfilePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ paddingTop: '12px' }}>
|
||||
<div style={{ paddingTop: '12px', display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
{!isOwnProfile && currentUser && (
|
||||
<button
|
||||
className={`btn ${isFollowing ? '' : 'btn-primary'}`}
|
||||
onClick={handleFollow}
|
||||
>
|
||||
{isFollowing ? 'Following' : 'Follow'}
|
||||
</button>
|
||||
<>
|
||||
{!isBlocked && (
|
||||
<button
|
||||
className={`btn ${isFollowing ? '' : 'btn-primary'}`}
|
||||
onClick={handleFollow}
|
||||
>
|
||||
{isFollowing ? 'Following' : 'Follow'}
|
||||
</button>
|
||||
)}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
style={{ padding: '8px' }}
|
||||
>
|
||||
<MoreHorizontal size={20} />
|
||||
</button>
|
||||
{showMenu && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 99,
|
||||
}}
|
||||
onClick={() => setShowMenu(false)}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: '100%',
|
||||
marginTop: '4px',
|
||||
background: 'var(--background-secondary)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
minWidth: '160px',
|
||||
zIndex: 100,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<button
|
||||
onClick={handleBlock}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
color: isBlocked ? 'var(--foreground)' : 'var(--error)',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{isBlocked ? 'Unblock user' : 'Block user'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isOwnProfile && (
|
||||
|
||||
Reference in New Issue
Block a user