mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Merge pull request #49 from ArnasDon/feat/mobile-inbox-single-pane
feat(mobile): inbox single-pane flow on phone, unchanged on desktop
This commit is contained in:
commit
a7dec71589
@ -10,6 +10,7 @@ import { MessageThread } from "@/components/inbox/message-thread";
|
|||||||
import { ContactSidebar } from "@/components/inbox/contact-sidebar";
|
import { ContactSidebar } from "@/components/inbox/contact-sidebar";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { WifiOff } from "lucide-react";
|
import { WifiOff } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function InboxPage() {
|
export default function InboxPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -191,6 +192,19 @@ export default function InboxPage() {
|
|||||||
[activeConversation?.id, router]
|
[activeConversation?.id, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Mobile "back" — deselect the conversation so the list pane comes
|
||||||
|
// back. Also clears the ?c= param so a refresh lands on the list
|
||||||
|
// instead of re-opening the thread the user just backed out of.
|
||||||
|
const handleCloseConversation = useCallback(() => {
|
||||||
|
setActiveConversation(null);
|
||||||
|
setActiveContact(null);
|
||||||
|
setMessages([]);
|
||||||
|
// Clearing the ref lets the deep-link auto-selector fire again if
|
||||||
|
// the user later visits /inbox?c=<same-id> — desirable UX.
|
||||||
|
autoSelectedForDeepLinkRef.current = null;
|
||||||
|
router.replace("/inbox", { scroll: false });
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
|
||||||
const handleMessagesLoaded = useCallback((loaded: Message[]) => {
|
const handleMessagesLoaded = useCallback((loaded: Message[]) => {
|
||||||
setMessages(loaded);
|
setMessages(loaded);
|
||||||
@ -224,8 +238,15 @@ export default function InboxPage() {
|
|||||||
[activeConversation]
|
[activeConversation]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// On mobile (<lg) we show a SINGLE pane — either the list or the
|
||||||
|
// thread — rather than cramming both side-by-side. Selecting a
|
||||||
|
// conversation slides the thread in; the thread's back button pops
|
||||||
|
// it back to the list. On lg+ both panes render side-by-side as
|
||||||
|
// before, unchanged.
|
||||||
|
const hasActiveConv = !!activeConversation;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="-m-6 flex h-[calc(100vh-3.5rem)] flex-col overflow-hidden">
|
<div className="-m-4 flex h-[calc(100vh-3.5rem)] flex-col overflow-hidden sm:-m-6">
|
||||||
{/* WhatsApp connection banner — in the flex column, not absolute,
|
{/* WhatsApp connection banner — in the flex column, not absolute,
|
||||||
so it pushes the panels down instead of overlapping them. */}
|
so it pushes the panels down instead of overlapping them. */}
|
||||||
{whatsappConnected === false && (
|
{whatsappConnected === false && (
|
||||||
@ -238,15 +259,33 @@ export default function InboxPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Left panel: Conversation list */}
|
{/* Left panel: Conversation list.
|
||||||
|
Hidden on mobile when a conversation is selected so the
|
||||||
|
thread can occupy the full width. Always visible on lg+. */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full flex-1 lg:flex-none",
|
||||||
|
hasActiveConv ? "hidden lg:flex" : "flex",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<ConversationList
|
<ConversationList
|
||||||
activeConversationId={activeConversation?.id ?? null}
|
activeConversationId={activeConversation?.id ?? null}
|
||||||
onSelect={handleSelectConversation}
|
onSelect={handleSelectConversation}
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
onConversationsLoaded={handleConversationsLoaded}
|
onConversationsLoaded={handleConversationsLoaded}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Center panel: Message thread */}
|
{/* Center panel: Message thread.
|
||||||
|
Hidden on mobile when no conversation is selected so the
|
||||||
|
list can occupy the full width. Always visible on lg+
|
||||||
|
(shows its own empty-state if no thread is picked yet). */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full flex-1 lg:flex",
|
||||||
|
hasActiveConv ? "flex" : "hidden lg:flex",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<MessageThread
|
<MessageThread
|
||||||
conversation={activeConversation}
|
conversation={activeConversation}
|
||||||
contact={activeContact}
|
contact={activeContact}
|
||||||
@ -255,9 +294,11 @@ export default function InboxPage() {
|
|||||||
onNewMessage={handleNewMessage}
|
onNewMessage={handleNewMessage}
|
||||||
onUpdateMessage={handleUpdateMessage}
|
onUpdateMessage={handleUpdateMessage}
|
||||||
onStatusChange={handleStatusChange}
|
onStatusChange={handleStatusChange}
|
||||||
|
onBack={handleCloseConversation}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Right panel: Contact sidebar (hidden on small screens) */}
|
{/* Right panel: Contact sidebar — desktop only. */}
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<ContactSidebar contact={activeContact} />
|
<ContactSidebar contact={activeContact} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -110,7 +110,10 @@ export function ConversationList({
|
|||||||
const activeFilter = FILTER_OPTIONS.find((o) => o.value === filter);
|
const activeFilter = FILTER_OPTIONS.find((o) => o.value === filter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-80 flex-col border-r border-slate-800 bg-slate-900">
|
// w-full on mobile so the list occupies the whole viewport when it's
|
||||||
|
// the single pane showing; fixed 320px on desktop where it shares the
|
||||||
|
// row with the thread + contact sidebar.
|
||||||
|
<div className="flex h-full w-full flex-col border-r border-slate-800 bg-slate-900 lg:w-80">
|
||||||
{/* Search + Filter */}
|
{/* Search + Filter */}
|
||||||
<div className="space-y-2 border-b border-slate-800 p-3">
|
<div className="space-y-2 border-b border-slate-800 p-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Clock,
|
Clock,
|
||||||
|
ArrowLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { format, isToday, isYesterday, differenceInHours } from "date-fns";
|
import { format, isToday, isYesterday, differenceInHours } from "date-fns";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@ -32,6 +33,13 @@ interface MessageThreadProps {
|
|||||||
onNewMessage: (message: Message) => void;
|
onNewMessage: (message: Message) => void;
|
||||||
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
|
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
|
||||||
onStatusChange: (conversationId: string, status: ConversationStatus) => void;
|
onStatusChange: (conversationId: string, status: ConversationStatus) => void;
|
||||||
|
/**
|
||||||
|
* On mobile, the thread is shown full-screen with the conversation list
|
||||||
|
* hidden. This callback lets the page deselect the active conversation
|
||||||
|
* and reveal the list again. Rendered as a back-arrow in the header on
|
||||||
|
* mobile only.
|
||||||
|
*/
|
||||||
|
onBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateSeparator(dateStr: string): string {
|
function formatDateSeparator(dateStr: string): string {
|
||||||
@ -72,6 +80,7 @@ export function MessageThread({
|
|||||||
onNewMessage,
|
onNewMessage,
|
||||||
onUpdateMessage,
|
onUpdateMessage,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
|
onBack,
|
||||||
}: MessageThreadProps) {
|
}: MessageThreadProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
@ -278,20 +287,33 @@ export function MessageThread({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col bg-slate-950">
|
<div className="flex flex-1 flex-col bg-slate-950">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between border-b border-slate-800 bg-slate-900 px-4 py-3">
|
<div className="flex items-center justify-between gap-2 border-b border-slate-800 bg-slate-900 px-3 py-3 sm:px-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white">
|
{/* Back-to-list button — mobile only. Hidden on lg+ where the
|
||||||
|
conversation list is always visible next to the thread. */}
|
||||||
|
{onBack && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onBack}
|
||||||
|
aria-label="Back to conversations"
|
||||||
|
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md text-slate-300 hover:bg-slate-800 hover:text-white lg:hidden"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-slate-700 text-sm font-medium text-white">
|
||||||
{displayName.charAt(0).toUpperCase()}
|
{displayName.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<h2 className="text-sm font-semibold text-white">{displayName}</h2>
|
<h2 className="truncate text-sm font-semibold text-white">{displayName}</h2>
|
||||||
<p className="text-xs text-slate-400">{contact.phone}</p>
|
<p className="truncate text-xs text-slate-400">{contact.phone}</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Session timer badge */}
|
{/* Session timer badge — hidden on the narrowest phones so
|
||||||
|
the name + back arrow keep their room. */}
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-2 gap-1 border-slate-700 text-[10px]",
|
"ml-1 hidden gap-1 border-slate-700 text-[10px] sm:inline-flex sm:ml-2",
|
||||||
sessionInfo.expired ? "text-red-400" : "text-emerald-400"
|
sessionInfo.expired ? "text-red-400" : "text-emerald-400"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user