AttachToConversation.tsx4.8 KBView on GitHub import { useMutation, useQuery } from '@tanstack/react-query';
import { Link2, Loader2, Search } from 'lucide-react';
import { useState, type ReactNode } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import type { InboxItem } from '@/modules/inbox/types';
import { useTRPC } from '@/providers/query-provider';
interface AttachToConversationProps {
item: InboxItem;
onAttached?: () => void;
/** Custom popover trigger (e.g. the top-row "link here" affordance). Defaults
* to the icon button used in the header actions. */
trigger?: ReactNode;
}
/**
* Top-bar control to attach / move a channel chat to a CRM conversation (deal).
* Search conversations, pick one → the channel's attach mutation binds the
* chat's events to it. LinkedIn/WhatsApp only (Slack channels are already
* conversation-scoped). See apps/mail/docs/omni-channel-inbox.md Phase 4.13.
*/
export function AttachToConversation({ item, onAttached, trigger }: AttachToConversationProps) {
const trpc = useTRPC();
const ref = item.ref;
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const search = useQuery({
...trpc.crm.searchConversationsMinimal.queryOptions({ query }),
enabled: open && query.trim().length >= 2,
});
const attachLinkedin = useMutation(trpc.linkedin.messaging.attachToDeal.mutationOptions());
const attachWhatsapp = useMutation(trpc.outbound.whatsapp.attachToDeal.mutationOptions());
const attaching = attachLinkedin.isPending || attachWhatsapp.isPending;
// Only chat channels can be re-homed; Slack is conversation-scoped already.
if (ref.kind !== 'linkedin' && ref.kind !== 'whatsapp') return null;
const attach = async (conversationId: string) => {
try {
if (ref.kind === 'linkedin') {
await attachLinkedin.mutateAsync({ chatId: ref.chatId, conversationId });
} else {
await attachWhatsapp.mutateAsync({ chatId: ref.chatId, conversationId });
}
setOpen(false);
setQuery('');
onAttached?.();
} catch {
toast.error('Failed to attach');
}
};
const results =
(search.data as { conversations?: { id: string; name?: string | null }[] } | undefined)
?.conversations ?? [];
return (
<Popover open={open} onOpenChange={setOpen}>
{trigger ? (
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
) : (
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer [&_svg]:size-4">
<Link2 className="text-[#9D9D9D]" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">Attach to conversation</TooltipContent>
</Tooltip>
)}
<PopoverContent align="end" className="w-72 p-2">
<div className="border-border mb-2 flex items-center gap-2 rounded-md border px-2">
<Search className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
<input
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search conversations…"
className="h-8 flex-1 bg-transparent text-sm outline-none"
/>
</div>
<div className="max-h-64 overflow-y-auto">
{attaching ? (
<div className="flex justify-center py-4">
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
</div>
) : query.trim().length < 2 ? (
<p className="text-muted-foreground px-2 py-3 text-center text-xs">
Type to search conversations.
</p>
) : search.isLoading ? (
<div className="flex justify-center py-4">
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
</div>
) : results.length === 0 ? (
<p className="text-muted-foreground px-2 py-3 text-center text-xs">No matches.</p>
) : (
<div className="flex flex-col gap-0.5">
{results.map((c) => (
<button
key=[redacted]
type="button"
onClick={() => void attach(c.id)}
className="hover:bg-subtleWhite flex cursor-pointer items-center rounded-md px-2 py-1.5 text-left text-sm transition-colors dark:hover:bg-[#202020]"
>
<span className="truncate">{c.name || 'Untitled conversation'}</span>
</button>
))}
</div>
)}
</div>
</PopoverContent>
</Popover>
);
}