2a65f78610
Renamed 'Latest' feed to 'Following' across the home and profile pages for clarity, and updated related UI text. Improved chat experience by adding a scroll-to-bottom button, tracking scroll position, and only auto-scrolling when the user is at the bottom. Updated default node description to 'A swarm social network node.' in multiple locations.
264 lines
8.1 KiB
TypeScript
264 lines
8.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useAuth } from '@/lib/contexts/AuthContext';
|
|
import { PostCard } from '@/components/PostCard';
|
|
import { Compose } from '@/components/Compose';
|
|
import { Post } from '@/lib/types';
|
|
import { EyeOff } from 'lucide-react';
|
|
|
|
interface FeedMeta {
|
|
score: number;
|
|
reasons: string[];
|
|
engagement: {
|
|
likes: number;
|
|
reposts: number;
|
|
replies: number;
|
|
};
|
|
}
|
|
|
|
export default function Home() {
|
|
const router = useRouter();
|
|
const { user } = useAuth();
|
|
const [posts, setPosts] = useState<Post[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
|
const [replyingTo, setReplyingTo] = useState<Post | null>(null);
|
|
const [feedType, setFeedType] = useState<'following' | 'curated'>('following');
|
|
const [feedMeta, setFeedMeta] = useState<{
|
|
algorithm: string;
|
|
windowHours: number;
|
|
seedLimit: number;
|
|
weights: {
|
|
engagement: number;
|
|
recency: number;
|
|
followBoost: number;
|
|
selfBoost: number;
|
|
};
|
|
} | null>(null);
|
|
|
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Redirect unauthenticated users to explore page
|
|
useEffect(() => {
|
|
if (user === null) {
|
|
router.push('/explore');
|
|
}
|
|
}, [user, router]);
|
|
|
|
const loadFeed = async (type: 'following' | 'curated', cursor?: string | null) => {
|
|
if (cursor) {
|
|
setLoadingMore(true);
|
|
} else {
|
|
setLoading(true);
|
|
}
|
|
try {
|
|
const endpoint = type === 'curated'
|
|
? `/api/posts?type=curated${cursor ? `&cursor=${cursor}` : ''}`
|
|
: `/api/posts?type=home${cursor ? `&cursor=${cursor}` : ''}`;
|
|
const res = await fetch(endpoint);
|
|
const data = await res.json();
|
|
|
|
if (cursor) {
|
|
setPosts(prev => [...prev, ...(data.posts || [])]);
|
|
} else {
|
|
setPosts(data.posts || []);
|
|
}
|
|
setFeedMeta(data.meta || null);
|
|
setNextCursor(data.nextCursor || null);
|
|
} catch {
|
|
if (!cursor) {
|
|
setPosts([]);
|
|
}
|
|
setFeedMeta(null);
|
|
setNextCursor(null);
|
|
} finally {
|
|
setLoading(false);
|
|
setLoadingMore(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
setPosts([]);
|
|
setNextCursor(null);
|
|
loadFeed(feedType);
|
|
}, [feedType]);
|
|
|
|
// Infinite scroll observer
|
|
useEffect(() => {
|
|
if (!loadMoreRef.current || !nextCursor || loadingMore) return;
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting && nextCursor && !loadingMore) {
|
|
loadFeed(feedType, nextCursor);
|
|
}
|
|
},
|
|
{ threshold: 0.1 }
|
|
);
|
|
|
|
observer.observe(loadMoreRef.current);
|
|
return () => observer.disconnect();
|
|
}, [nextCursor, loadingMore, feedType]);
|
|
|
|
const handlePost = async (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string, isNsfw?: boolean) => {
|
|
// Check if we're replying to a swarm post
|
|
let swarmReplyTo: { postId: string; nodeDomain: string } | undefined;
|
|
let localReplyToId: string | undefined = replyToId;
|
|
|
|
if (replyingTo?.isSwarm && replyingTo.nodeDomain && replyingTo.originalPostId) {
|
|
// This is a reply to a swarm post - send to the origin node
|
|
swarmReplyTo = {
|
|
postId: replyingTo.originalPostId,
|
|
nodeDomain: replyingTo.nodeDomain,
|
|
};
|
|
localReplyToId = undefined; // Don't set local replyToId for swarm posts
|
|
}
|
|
|
|
const res = await fetch('/api/posts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content, mediaIds, linkPreview, replyToId: localReplyToId, swarmReplyTo, isNsfw }),
|
|
});
|
|
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (feedType === 'curated') {
|
|
setPosts([]);
|
|
setNextCursor(null);
|
|
loadFeed('curated');
|
|
} else {
|
|
setPosts([{ ...data.post, author: user }, ...posts]);
|
|
}
|
|
setReplyingTo(null);
|
|
}
|
|
};
|
|
|
|
const handleLike = async (postId: string, currentLiked: boolean) => {
|
|
const method = currentLiked ? 'DELETE' : 'POST';
|
|
await fetch(`/api/posts/${postId}/like`, { method });
|
|
};
|
|
|
|
const handleRepost = async (postId: string, currentReposted: boolean) => {
|
|
const method = currentReposted ? 'DELETE' : 'POST';
|
|
await fetch(`/api/posts/${postId}/repost`, { method });
|
|
};
|
|
|
|
const handleDelete = (postId: string) => {
|
|
setPosts(prev => prev.filter(p => p.id !== postId));
|
|
};
|
|
|
|
// Show loading while checking auth
|
|
if (user === null) {
|
|
return (
|
|
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
|
|
Loading...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<header style={{
|
|
padding: '16px',
|
|
borderBottom: '1px solid var(--border)',
|
|
position: 'sticky',
|
|
top: 0,
|
|
background: 'var(--background)',
|
|
zIndex: 10,
|
|
backdropFilter: 'blur(12px)',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>Home</h1>
|
|
<div className="feed-toggle">
|
|
<button
|
|
className={`feed-toggle-btn ${feedType === 'following' ? 'active' : ''}`}
|
|
onClick={() => setFeedType('following')}
|
|
>
|
|
Following
|
|
</button>
|
|
<button
|
|
className={`feed-toggle-btn ${feedType === 'curated' ? 'active' : ''}`}
|
|
onClick={() => setFeedType('curated')}
|
|
>
|
|
Curated
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<Compose
|
|
onPost={handlePost}
|
|
replyingTo={replyingTo}
|
|
onCancelReply={() => setReplyingTo(null)}
|
|
/>
|
|
|
|
{feedType === 'following' && (
|
|
<div className="feed-meta card">
|
|
<div className="feed-meta-title">Following feed</div>
|
|
<div className="feed-meta-body">
|
|
This feed shows posts from accounts you follow in chronological order, with the most recent posts appearing first.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{feedType === 'curated' && feedMeta && (
|
|
<div className="feed-meta card">
|
|
<div className="feed-meta-title">Curated feed</div>
|
|
<div className="feed-meta-body">
|
|
This feed highlights fresh posts and active discussions, with a boost for people you follow. It is designed to surface what matters without hiding your own activity.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
|
|
Loading...
|
|
</div>
|
|
) : posts.length === 0 ? (
|
|
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
|
|
{feedType === 'curated' ? (
|
|
<>
|
|
<p>No posts from the swarm yet</p>
|
|
<p style={{ fontSize: '13px', marginTop: '8px' }}>
|
|
The curated feed shows posts from other nodes in the Synapsis network.
|
|
Check back later as nodes are discovered, or switch to Following to see posts from people you follow.
|
|
</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<p>No posts yet</p>
|
|
<p style={{ fontSize: '13px', marginTop: '8px' }}>Be the first to post something!</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
{posts.map(post => (
|
|
<PostCard
|
|
key={post.id}
|
|
post={post}
|
|
onLike={handleLike}
|
|
onRepost={handleRepost}
|
|
onDelete={handleDelete}
|
|
onComment={(p) => {
|
|
setReplyingTo(p);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}}
|
|
/>
|
|
))}
|
|
{/* Infinite scroll trigger */}
|
|
<div ref={loadMoreRef} style={{ padding: '24px', textAlign: 'center' }}>
|
|
{loadingMore && (
|
|
<span style={{ color: 'var(--foreground-tertiary)' }}>Loading more...</span>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|