Constrain messages page scrolling

This commit is contained in:
lemon
2026-06-20 20:01:03 -07:00
parent 7a4259db6a
commit 5553a31ffa
2 changed files with 39 additions and 25 deletions
+6 -3
View File
@@ -119,7 +119,7 @@ function SiteFooter() {
* form/prose-style pages, wide (full width) for landing / dashboard / detail
* pages that render their own internal layout.
*/
function FundraiserLayout({ narrow }: { narrow: boolean }) {
function FundraiserLayout({ narrow, hideFooter }: { narrow: boolean; hideFooter?: boolean }) {
return (
<div className="min-h-dvh flex flex-col bg-background">
<TopNav />
@@ -130,7 +130,7 @@ function FundraiserLayout({ narrow }: { narrow: boolean }) {
<Outlet />
</div>
</Suspense>
<SiteFooter />
{!hideFooter && <SiteFooter />}
</div>
);
}
@@ -200,6 +200,10 @@ export function AppRouter() {
<Route path="/remoteloginsuccess" element={<RemoteLoginSuccessPage />} />
</Route>
<Route element={<FundraiserLayout narrow={false} hideFooter />}>
<Route path="/messages" element={<MessagesPage />} />
</Route>
{/* Wide layout — no max-width on the center column. Used by landing /
list / detail pages that render their own internal width
constraints. */}
@@ -218,7 +222,6 @@ export function AppRouter() {
<Route path="/pledges" element={<ActionsPage />} />
<Route path="/pledges/new" element={<CreateActionPage />} />
<Route path="/dashboard" element={<EventDashboardPage />} />
<Route path="/messages" element={<MessagesPage />} />
<Route path="/i/*" element={<ExternalContentPage />} />
{/* About page + Donor / Recipient guides. Full-bleed landing-style
layouts that render their own internal max-widths. */}
+33 -22
View File
@@ -3,11 +3,12 @@ import { useEffect, useMemo, useRef, useState, type FormEvent, type KeyboardEven
import { Navigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useInView } from 'react-intersection-observer';
import { ArrowLeft, Inbox, Loader2, Lock, MessageSquare, Send } from 'lucide-react';
import { ArrowLeft, Loader2, Lock, MessageSquare, Search, Send } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Skeleton } from '@/components/ui/skeleton';
import { Textarea } from '@/components/ui/textarea';
@@ -176,13 +177,10 @@ function MessageThread({ conversation, onBack }: { conversation: Conversation; o
<AvatarImage src={picture} alt={name} />
<AvatarFallback>{name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate font-semibold">{name}</p>
<p className="truncate text-xs text-muted-foreground">{t('messages.subtitle')}</p>
</div>
<p className="min-w-0 flex-1 truncate font-semibold">{name}</p>
</div>
<ScrollArea className="flex-1 bg-gradient-to-b from-muted/30 via-background to-background px-4">
<ScrollArea className="min-h-0 flex-1 bg-gradient-to-b from-muted/30 via-background to-background px-4">
<div className="space-y-3 py-5">
{isLoading ? (
<div className="space-y-3">
@@ -232,6 +230,7 @@ export function MessagesPage() {
pageCount,
} = useDirectMessages();
const [selectedPeer, setSelectedPeer] = useState<string | null>(null);
const [search, setSearch] = useState('');
const { ref: olderMessagesRef, inView: olderMessagesInView } = useInView({ threshold: 0, rootMargin: '300px' });
useSeoMeta({
@@ -244,6 +243,15 @@ export function MessagesPage() {
[conversations, selectedPeer],
);
const filteredConversations = useMemo(() => {
const query = search.trim().toLocaleLowerCase();
if (!query) return conversations;
return conversations?.filter((conversation) => {
const latest = conversation.latest.content?.toLocaleLowerCase() ?? '';
return conversation.peer.toLocaleLowerCase().includes(query) || latest.includes(query);
});
}, [conversations, search]);
useEffect(() => {
if (pageCount === 1 && hasNextPage && !isFetchingNextPage) {
void fetchNextPage();
@@ -263,33 +271,36 @@ export function MessagesPage() {
const hasDmSupport = !!user.signer.nip04;
return (
<main className="min-h-[calc(100dvh-4rem)] bg-background">
<main className="h-[calc(100dvh-4rem)] overflow-hidden bg-background">
{!hasDmSupport ? (
<div className="flex min-h-[calc(100dvh-4rem)] items-center justify-center border-y px-8 py-12 text-center">
<div className="flex h-full items-center justify-center border-y px-8 py-12 text-center">
<p className="mx-auto max-w-sm text-muted-foreground">
{t('messages.unsupported')}
</p>
</div>
) : (
<div className="grid min-h-[calc(100dvh-4rem)] overflow-hidden border-y bg-background md:grid-cols-[22rem_1fr]">
<div className="grid h-full min-h-0 overflow-hidden border-y bg-background md:grid-cols-[22rem_1fr]">
{/* Conversation list */}
<div
className={cn(
'min-h-0 flex-col border-r bg-muted/40',
'h-full min-h-0 flex-col border-r bg-muted/40',
selected && 'hidden md:flex',
!selected && 'flex',
)}
>
<div className="border-b bg-card/80 p-4 backdrop-blur">
<div className="flex items-center justify-between gap-3">
<div>
<h2 className="text-sm font-semibold">{t('messages.conversations')}</h2>
<p className="text-xs text-muted-foreground">NIP-04</p>
</div>
<Inbox className="size-5 text-muted-foreground" />
<div className="border-b bg-card/80 p-3 backdrop-blur">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('nav.search')}
aria-label={t('nav.search')}
className="h-10 rounded-xl bg-background pl-9"
/>
</div>
</div>
<ScrollArea className="flex-1">
<ScrollArea className="min-h-0 flex-1">
<div className="space-y-2 p-3">
{isLoading ? (
Array.from({ length: 5 }).map((_, i) => (
@@ -301,8 +312,8 @@ export function MessagesPage() {
</div>
</div>
))
) : conversations && conversations.length > 0 ? (
conversations.map((conversation) => (
) : filteredConversations && filteredConversations.length > 0 ? (
filteredConversations.map((conversation) => (
<ConversationRow
key={conversation.peer}
conversation={conversation}
@@ -317,7 +328,7 @@ export function MessagesPage() {
</p>
</div>
)}
{hasNextPage && conversations && conversations.length > 0 && (
{hasNextPage && conversations && conversations.length > 0 && !search.trim() && (
<div ref={olderMessagesRef} className="flex justify-center py-4">
{isFetchingNextPage && <Loader2 className="size-4 animate-spin text-muted-foreground" />}
</div>
@@ -327,7 +338,7 @@ export function MessagesPage() {
</div>
{/* Thread */}
<div className={cn('min-w-0', !selected && 'hidden md:block')}>
<div className={cn('h-full min-h-0 min-w-0', !selected && 'hidden md:block')}>
{selected ? (
<MessageThread conversation={selected} onBack={() => setSelectedPeer(null)} />
) : (