Update feed labels and improve chat UX

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.
This commit is contained in:
Christopher
2026-01-27 13:29:15 -08:00
parent a68aee8758
commit 2a65f78610
5 changed files with 104 additions and 29 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ export async function GET() {
if (!node) {
return NextResponse.json({
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
description: process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A federated social network node.',
description: process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A swarm social network node.',
accentColor: process.env.NEXT_PUBLIC_ACCENT_COLOR || '#FFFFFF',
domain,
admins,
@@ -52,7 +52,7 @@ export async function GET() {
console.error('Node info error:', error);
return NextResponse.json({
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
description: process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A federated social network node.',
description: process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A swarm social network node.',
admins: [],
});
}
+74 -8
View File
@@ -61,6 +61,28 @@ export default function ChatPage() {
const [encryptionChecked, setEncryptionChecked] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const [isAtBottom, setIsAtBottom] = useState(true);
// Check if user is scrolled to bottom
const checkIfAtBottom = () => {
if (!messagesContainerRef.current) return true;
const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current;
const threshold = 100; // pixels from bottom
return scrollHeight - scrollTop - clientHeight < threshold;
};
// Handle scroll to track if user is at bottom
const handleScroll = () => {
setIsAtBottom(checkIfAtBottom());
};
// Scroll to bottom manually
const scrollToBottom = () => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
};
// Wait for encryption to be ready before showing UI
useEffect(() => {
@@ -106,12 +128,12 @@ export default function ChatPage() {
}
}, [selectedConversation, hasKeys]);
// Auto-scroll to bottom of messages
// Auto-scroll to bottom of messages only if user was already at bottom
useEffect(() => {
if (messagesEndRef.current) {
if (messagesEndRef.current && isAtBottom) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [messages]);
}, [messages, isAtBottom]);
const fetchRecipientKey = async (handle: string) => {
try {
@@ -439,9 +461,9 @@ export default function ChatPage() {
// Thread View
if (selectedConversation) {
return (
<>
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', maxWidth: '600px', margin: '0 auto' }}>
{/* Header */}
<div className="post" style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--background)', borderBottom: '1px solid var(--border)' }}>
<div className="post" style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--background)', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<button
onClick={() => setSelectedConversation(null)}
@@ -479,7 +501,17 @@ export default function ChatPage() {
</div>
{/* Messages */}
<div style={{ padding: '16px', minHeight: 'calc(100vh - 250px)' }}>
<div
ref={messagesContainerRef}
onScroll={handleScroll}
style={{
padding: '16px',
flex: 1,
overflowY: 'auto',
paddingBottom: '16px',
position: 'relative'
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{messages.map(msg => (
<div key={msg.id} style={{
@@ -524,10 +556,44 @@ export default function ChatPage() {
))}
<div ref={messagesEndRef} />
</div>
{/* Scroll to bottom button */}
{!isAtBottom && (
<button
onClick={scrollToBottom}
style={{
position: 'absolute',
bottom: '16px',
right: '24px',
width: '40px',
height: '40px',
borderRadius: '50%',
background: 'var(--accent)',
color: '#000',
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
zIndex: 5
}}
aria-label="Scroll to bottom"
>
</button>
)}
</div>
{/* Input */}
<div className="compose">
<div
className="compose"
style={{
borderTop: '1px solid var(--border)',
background: 'var(--background)',
flexShrink: 0
}}
>
<form onSubmit={sendMessage} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="text"
@@ -546,7 +612,7 @@ export default function ChatPage() {
</button>
</form>
</div>
</>
</div>
);
}
+15 -6
View File
@@ -27,7 +27,7 @@ export default function Home() {
const [loadingMore, setLoadingMore] = useState(false);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const [replyingTo, setReplyingTo] = useState<Post | null>(null);
const [feedType, setFeedType] = useState<'latest' | 'curated'>('latest');
const [feedType, setFeedType] = useState<'following' | 'curated'>('following');
const [feedMeta, setFeedMeta] = useState<{
algorithm: string;
windowHours: number;
@@ -49,7 +49,7 @@ export default function Home() {
}
}, [user, router]);
const loadFeed = async (type: 'latest' | 'curated', cursor?: string | null) => {
const loadFeed = async (type: 'following' | 'curated', cursor?: string | null) => {
if (cursor) {
setLoadingMore(true);
} else {
@@ -175,10 +175,10 @@ export default function Home() {
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>Home</h1>
<div className="feed-toggle">
<button
className={`feed-toggle-btn ${feedType === 'latest' ? 'active' : ''}`}
onClick={() => setFeedType('latest')}
className={`feed-toggle-btn ${feedType === 'following' ? 'active' : ''}`}
onClick={() => setFeedType('following')}
>
Latest
Following
</button>
<button
className={`feed-toggle-btn ${feedType === 'curated' ? 'active' : ''}`}
@@ -196,6 +196,15 @@ export default function Home() {
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>
@@ -216,7 +225,7 @@ export default function Home() {
<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 Latest to see posts from people you follow.
Check back later as nodes are discovered, or switch to Following to see posts from people you follow.
</p>
</>
) : (
+12 -12
View File
@@ -636,18 +636,6 @@ export default function ProfilePage() {
)}
<div style={{ display: 'flex', gap: '16px', marginTop: '12px' }}>
<button
onClick={() => setActiveTab('following')}
style={{
background: 'none',
border: 'none',
color: 'var(--foreground)',
cursor: 'pointer',
}}
>
<strong>{user.followingCount}</strong>{' '}
<span style={{ color: 'var(--foreground-tertiary)' }}>Following</span>
</button>
<button
onClick={() => setActiveTab('followers')}
style={{
@@ -660,6 +648,18 @@ export default function ProfilePage() {
<strong>{user.followersCount}</strong>{' '}
<span style={{ color: 'var(--foreground-tertiary)' }}>Followers</span>
</button>
<button
onClick={() => setActiveTab('following')}
style={{
background: 'none',
border: 'none',
color: 'var(--foreground)',
cursor: 'pointer',
}}
>
<strong>{user.followingCount}</strong>{' '}
<span style={{ color: 'var(--foreground-tertiary)' }}>Following</span>
</button>
</div>
</div>
</div>
+1 -1
View File
@@ -10,7 +10,7 @@ interface Admin {
}
export function RightSidebar() {
const fallbackDescription = process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A federated social network node.';
const fallbackDescription = process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A swarm social network node.';
const [nodeInfo, setNodeInfo] = useState({
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
description: fallbackDescription,