From 869f3ad00113ea8bccf24cb21be66f79aa4a87d1 Mon Sep 17 00:00:00 2001 From: limiteinductive Date: Sun, 29 Mar 2026 21:28:24 +0200 Subject: [PATCH] =?UTF-8?q?scroll-to-bottom=20=E2=86=93=20button=20above?= =?UTF-8?q?=20compose=20when=20scrolled=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Appears when >150px from bottom - Smooth scrolls on click - Sending a message auto-scrolls to bottom - Proper scroll tracking via viewport event listener Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/colony/src/App.tsx | 49 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/ui/colony/src/App.tsx b/ui/colony/src/App.tsx index b81e1d5..b93dd99 100644 --- a/ui/colony/src/App.tsx +++ b/ui/colony/src/App.tsx @@ -45,6 +45,7 @@ export default function App() { const [loading, setLoading] = useState(false); const [replyTo, setReplyTo] = useState<{ id: string; username: string; content: string } | null>(null); const [sheetOpen, setSheetOpen] = useState(false); + const [showScrollDown, setShowScrollDown] = useState(false); const scrollRef = useRef(null); const prevMsgCountRef = useRef(0); const activeChannelRef = useRef(activeChannelId); @@ -97,14 +98,36 @@ export default function App() { loadMessages(); }, [activeChannelId, loadMessages]); + function getViewport() { + const el = scrollRef.current as unknown as HTMLElement | null; + return el?.querySelector('[data-slot="scroll-area-viewport"]') as HTMLElement | null ?? el; + } + + function scrollToBottom(smooth = false) { + const vp = getViewport(); + if (vp) vp.scrollTo({ top: vp.scrollHeight, behavior: smooth ? "smooth" : "instant" }); + } + // Auto-scroll only on new messages useEffect(() => { - if (messages.length > prevMsgCountRef.current && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + if (messages.length > prevMsgCountRef.current) { + scrollToBottom(); } prevMsgCountRef.current = messages.length; }, [messages]); + // Track scroll position for scroll-down button + useEffect(() => { + const vp = getViewport(); + if (!vp) return; + function onScroll() { + const v = vp!; + setShowScrollDown(v.scrollHeight - v.scrollTop - v.clientHeight > 150); + } + vp.addEventListener("scroll", onScroll, { passive: true }); + return () => vp.removeEventListener("scroll", onScroll); + }); + const messagesById = new Map(messages.map((m) => [m.id, m])); const activeChannel = channels.find((c) => c.id === activeChannelId); @@ -208,6 +231,19 @@ export default function App() { )} + {/* Scroll-to-bottom button */} + {showScrollDown && ( +
+ +
+ )} + {activeChannelId && ( setReplyTo(null)} onMessageSent={() => { loadMessages(); - // Force scroll to bottom after sending - setTimeout(() => { - if (scrollRef.current) { - const el = scrollRef.current as unknown as HTMLElement; - const viewport = el.querySelector('[data-slot="scroll-area-viewport"]') || el; - viewport.scrollTop = viewport.scrollHeight; - } - }, 100); + setTimeout(() => scrollToBottom(), 100); }} /> )}