diff --git a/src/app/(auth)/auth/profile/page.tsx b/src/app/(auth)/auth/profile/page.tsx index 3893acb..dbec8ba 100644 --- a/src/app/(auth)/auth/profile/page.tsx +++ b/src/app/(auth)/auth/profile/page.tsx @@ -59,7 +59,7 @@ export default async function ProfileSetupPage({
diff --git a/src/components/chunked-list.tsx b/src/components/chunked-list.tsx new file mode 100644 index 0000000..643fd17 --- /dev/null +++ b/src/components/chunked-list.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { InfiniteScrollSentinel } from "@/components/infinite-scroll-sentinel"; + +// Renders one long list, revealing the next chunk as the end scrolls into +// view, so everything stays reachable by scrolling instead of paging. +export function ChunkedList({ + chunkSize = 24, + className, + items, + renderItem, + resetKey, +}: ChunkedListProps) { + return ( + + ); +} + +type ChunkedListProps = { + chunkSize?: number; + className?: string; + items: T[]; + renderItem: (item: T, index: number) => ReactNode; + resetKey?: string; +}; + +function ChunkedListContent({ + chunkSize, + className, + items, + renderItem, +}: Omit, "chunkSize" | "resetKey"> & { + chunkSize: number; +}) { + const [visibleCount, setVisibleCount] = useState(chunkSize); + const visibleItems = items.slice(0, visibleCount); + + return ( + <> +
{visibleItems.map(renderItem)}
+ {/* Keyed by count so a sentinel still in view after a chunk renders + gets a fresh observer and reveals the next chunk too. */} + setVisibleCount((count) => count + chunkSize)} + /> + + ); +} diff --git a/src/components/friends/direct-messages.tsx b/src/components/friends/direct-messages.tsx index 61b0b9f..f128d0f 100644 --- a/src/components/friends/direct-messages.tsx +++ b/src/components/friends/direct-messages.tsx @@ -112,11 +112,11 @@ export function DirectMessages({ Boolean(initialFriendId && remoteClient), ); const [isLoadingEarlier, setIsLoadingEarlier] = useState(false); - const [isSending, setIsSending] = useState(false); const [feedback, setFeedback] = useState(null); const [isClosing, setIsClosing] = useState(false); const listRef = useRef(null); const chatRef = useRef(null); + const composerRef = useRef(null); const closeTimerRef = useRef(null); const finishCloseRef = useRef<() => void>(() => {}); const loadSequenceRef = useRef(0); @@ -294,6 +294,14 @@ export function DirectMessages({ }; }, [isConversationOpen]); + useEffect(() => { + const composer = composerRef.current; + if (!composer) return; + + composer.style.height = "auto"; + composer.style.height = `${Math.min(composer.scrollHeight, 112)}px`; + }, [draft, isConversationOpen]); + function setComposerFocused(focused: boolean) { document.body.classList.toggle("mac-chat-composer-active", focused); window.dispatchEvent(new Event("resize")); @@ -488,7 +496,9 @@ export function DirectMessages({ if (counterpartId === selectedFriendId) { shouldScrollToBottomRef.current = true; setMessages((current) => - mergeMessages(current, [directMessageFromInsertRow(row)]), + mergeMessages(dropMatchingPending(current, row), [ + directMessageFromInsertRow(row), + ]), ); if ( @@ -536,15 +546,11 @@ export function DirectMessages({ } } + // Like the group chat, sends don't block the composer: each message shows + // as pending straight away and several can be in flight at once. async function sendMessage(messageBody = draft) { const body = messageBody.trim(); - if ( - !body || - !selectedFriend || - !currentUserId || - !remoteClient || - isSending - ) { + if (!body || !selectedFriend || !currentUserId || !remoteClient) { return; } @@ -561,7 +567,6 @@ export function DirectMessages({ setDraft(""); setFeedback(null); - setIsSending(true); shouldScrollToBottomRef.current = true; setMessages((current) => mergeMessages(current, [pendingMessage])); @@ -600,8 +605,6 @@ export function DirectMessages({ ), ); setFeedback(getErrorMessage(error, "Message could not be sent.")); - } finally { - setIsSending(false); } } @@ -631,11 +634,11 @@ export function DirectMessages({
+ onToggleMute(selectedFriend)} + />