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:
@@ -34,7 +34,7 @@ export async function GET() {
|
|||||||
if (!node) {
|
if (!node) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
|
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',
|
accentColor: process.env.NEXT_PUBLIC_ACCENT_COLOR || '#FFFFFF',
|
||||||
domain,
|
domain,
|
||||||
admins,
|
admins,
|
||||||
@@ -52,7 +52,7 @@ export async function GET() {
|
|||||||
console.error('Node info error:', error);
|
console.error('Node info error:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
|
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: [],
|
admins: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-8
@@ -61,6 +61,28 @@ export default function ChatPage() {
|
|||||||
const [encryptionChecked, setEncryptionChecked] = useState(false);
|
const [encryptionChecked, setEncryptionChecked] = useState(false);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
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
|
// Wait for encryption to be ready before showing UI
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -106,12 +128,12 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
}, [selectedConversation, hasKeys]);
|
}, [selectedConversation, hasKeys]);
|
||||||
|
|
||||||
// Auto-scroll to bottom of messages
|
// Auto-scroll to bottom of messages only if user was already at bottom
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current) {
|
if (messagesEndRef.current && isAtBottom) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages, isAtBottom]);
|
||||||
|
|
||||||
const fetchRecipientKey = async (handle: string) => {
|
const fetchRecipientKey = async (handle: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -439,9 +461,9 @@ export default function ChatPage() {
|
|||||||
// Thread View
|
// Thread View
|
||||||
if (selectedConversation) {
|
if (selectedConversation) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', maxWidth: '600px', margin: '0 auto' }}>
|
||||||
{/* Header */}
|
{/* 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' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedConversation(null)}
|
onClick={() => setSelectedConversation(null)}
|
||||||
@@ -479,7 +501,17 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{/* 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' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
{messages.map(msg => (
|
{messages.map(msg => (
|
||||||
<div key={msg.id} style={{
|
<div key={msg.id} style={{
|
||||||
@@ -524,10 +556,44 @@ export default function ChatPage() {
|
|||||||
))}
|
))}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Input */}
|
{/* 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' }}>
|
<form onSubmit={sendMessage} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -546,7 +612,7 @@ export default function ChatPage() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-6
@@ -27,7 +27,7 @@ export default function Home() {
|
|||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||||||
const [replyingTo, setReplyingTo] = useState<Post | 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<{
|
const [feedMeta, setFeedMeta] = useState<{
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
windowHours: number;
|
windowHours: number;
|
||||||
@@ -49,7 +49,7 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, [user, router]);
|
}, [user, router]);
|
||||||
|
|
||||||
const loadFeed = async (type: 'latest' | 'curated', cursor?: string | null) => {
|
const loadFeed = async (type: 'following' | 'curated', cursor?: string | null) => {
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
setLoadingMore(true);
|
setLoadingMore(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -175,10 +175,10 @@ export default function Home() {
|
|||||||
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>Home</h1>
|
<h1 style={{ fontSize: '18px', fontWeight: 600 }}>Home</h1>
|
||||||
<div className="feed-toggle">
|
<div className="feed-toggle">
|
||||||
<button
|
<button
|
||||||
className={`feed-toggle-btn ${feedType === 'latest' ? 'active' : ''}`}
|
className={`feed-toggle-btn ${feedType === 'following' ? 'active' : ''}`}
|
||||||
onClick={() => setFeedType('latest')}
|
onClick={() => setFeedType('following')}
|
||||||
>
|
>
|
||||||
Latest
|
Following
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`feed-toggle-btn ${feedType === 'curated' ? 'active' : ''}`}
|
className={`feed-toggle-btn ${feedType === 'curated' ? 'active' : ''}`}
|
||||||
@@ -196,6 +196,15 @@ export default function Home() {
|
|||||||
onCancelReply={() => setReplyingTo(null)}
|
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 && (
|
{feedType === 'curated' && feedMeta && (
|
||||||
<div className="feed-meta card">
|
<div className="feed-meta card">
|
||||||
<div className="feed-meta-title">Curated feed</div>
|
<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>No posts from the swarm yet</p>
|
||||||
<p style={{ fontSize: '13px', marginTop: '8px' }}>
|
<p style={{ fontSize: '13px', marginTop: '8px' }}>
|
||||||
The curated feed shows posts from other nodes in the Synapsis network.
|
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>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+12
-12
@@ -636,18 +636,6 @@ export default function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '16px', marginTop: '12px' }}>
|
<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
|
<button
|
||||||
onClick={() => setActiveTab('followers')}
|
onClick={() => setActiveTab('followers')}
|
||||||
style={{
|
style={{
|
||||||
@@ -660,6 +648,18 @@ export default function ProfilePage() {
|
|||||||
<strong>{user.followersCount}</strong>{' '}
|
<strong>{user.followersCount}</strong>{' '}
|
||||||
<span style={{ color: 'var(--foreground-tertiary)' }}>Followers</span>
|
<span style={{ color: 'var(--foreground-tertiary)' }}>Followers</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface Admin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RightSidebar() {
|
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({
|
const [nodeInfo, setNodeInfo] = useState({
|
||||||
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
|
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
|
||||||
description: fallbackDescription,
|
description: fallbackDescription,
|
||||||
|
|||||||
Reference in New Issue
Block a user