@burdenoff/microfe-housingvista 2026.802.1 → 2026.830.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/housingvista/hooks/index.d.ts +2 -2
- package/dist/housingvista/hooks/index.d.ts.map +1 -1
- package/dist/housingvista/hooks/index.js.map +1 -1
- package/dist/housingvista/hooks/messages.d.ts +18 -11
- package/dist/housingvista/hooks/messages.d.ts.map +1 -1
- package/dist/housingvista/hooks/messages.js +33 -13
- package/dist/housingvista/hooks/messages.js.map +1 -1
- package/dist/housingvista/hooks/platform.d.ts +54 -2
- package/dist/housingvista/hooks/platform.d.ts.map +1 -1
- package/dist/housingvista/hooks/platform.js +63 -18
- package/dist/housingvista/hooks/platform.js.map +1 -1
- package/dist/housingvista/pages/HomePage.js +140 -142
- package/dist/housingvista/pages/HomePage.js.map +1 -1
- package/dist/housingvista/pages/chat/ChatPage.d.ts +4 -2
- package/dist/housingvista/pages/chat/ChatPage.d.ts.map +1 -1
- package/dist/housingvista/pages/chat/ChatPage.js +305 -151
- package/dist/housingvista/pages/chat/ChatPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.d.ts.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.js +427 -312
- package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.d.ts.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.js +491 -374
- package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
- package/dist/housingvista/pages/transactions/TransactionsPage.d.ts.map +1 -1
- package/dist/housingvista/pages/transactions/TransactionsPage.js +153 -155
- package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
- package/dist/shared/hooks/index.d.ts +2 -0
- package/dist/shared/hooks/index.d.ts.map +1 -0
- package/dist/shared/hooks/index.js +1 -0
- package/dist/shared/hooks/useTr.d.ts +17 -0
- package/dist/shared/hooks/useTr.d.ts.map +1 -0
- package/dist/shared/hooks/useTr.js +14 -0
- package/dist/shared/hooks/useTr.js.map +1 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatPage.js","names":[],"sources":["../../../../src/housingvista/pages/chat/ChatPage.tsx"],"sourcesContent":["/**\n * Messages — the real HousingVista inbox. A two-pane conversations UI (thread\n * list + chat thread with bubbles) backed by wspace-conversations-svc:\n * - `useHousingVistaConversations` lists the user's product threads,\n * - `useConversationMessages` streams the selected thread,\n * - `useSendMessage` posts new messages.\n * Everything is persisted server-side; when there are no threads yet we show a\n * tasteful empty state (never fabricated rows). Fully responsive: on mobile the\n * list and the open thread swap places (back button), on desktop they sit\n * side-by-side.\n * @module housingvista\n */\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useNavigate } from 'react-router';\nimport { ArrowLeft, MapPin, MessageSquare, Search, Send } from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState, StatCard } from '../../components';\nimport { useHousingVistaConversations, type HvConversationSummary } from '../../hooks/messages';\nimport { useConversationMessages, useSendMessage } from '../../hooks';\nimport { useHousingVista } from '../../context/HousingVistaContext';\nimport { notify } from '../../lib/toast';\nimport { cn, getRelativeTime } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\n/** Deterministic accent for an avatar chip, derived from the thread title. */\nconst AVATAR_ACCENTS = [\n 'bg-status-info-bg-subtle text-status-info-text',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-action-primary-bg/15 text-primary',\n];\n\nfunction initials(title: string): string {\n const parts = title.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\nfunction accentFor(id: string): string {\n let sum = 0;\n for (let i = 0; i < id.length; i += 1) sum += id.charCodeAt(i);\n return AVATAR_ACCENTS[sum % AVATAR_ACCENTS.length];\n}\n\nfunction messageTime(iso: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '';\n return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });\n}\n\nfunction Avatar({ title, id, size = 'md' }: { title: string; id: string; size?: 'sm' | 'md' }) {\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full font-semibold',\n size === 'md' ? 'h-10 w-10 text-sm' : 'h-9 w-9 text-xs',\n accentFor(id)\n )}\n aria-hidden\n >\n {initials(title)}\n </span>\n );\n}\n\nexport function ChatPage() {\n useTrackPageView('housingvista.chat.viewed');\n const navigate = useNavigate();\n const { currentUser } = useHousingVista();\n const currentUserId = currentUser?.id ?? '';\n\n const { data: conversations, loading, refetch } = useHousingVistaConversations();\n const { sendMessage, loading: sending } = useSendMessage();\n\n const [selectedId, setSelectedId] = useState<string | undefined>();\n const [query, setQuery] = useState('');\n const [draft, setDraft] = useState('');\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Default-select the first thread on desktop once data lands.\n useEffect(() => {\n if (!selectedId && conversations.length > 0 && typeof window !== 'undefined' && window.innerWidth >= 1024) {\n setSelectedId(conversations[0].id);\n }\n }, [conversations, selectedId]);\n\n const selected: HvConversationSummary | undefined = useMemo(\n () => conversations.find((c) => c.id === selectedId),\n [conversations, selectedId]\n );\n\n const { data: messages, loading: loadingMessages } = useConversationMessages(selectedId);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });\n }, [messages, selectedId]);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return conversations;\n return conversations.filter(\n (c) => c.title.toLowerCase().includes(q) || c.lastMessageText.toLowerCase().includes(q)\n );\n }, [conversations, query]);\n\n const unreadThreads = conversations.filter((c) => c.unreadCount > 0).length;\n\n const handleSend = async () => {\n const text = draft.trim();\n if (!text || !selectedId) return;\n try {\n await sendMessage(selectedId, text);\n setDraft('');\n void refetch();\n } catch (err) {\n notify.error('Message not sent', (err as Error).message);\n }\n };\n\n const emptyInbox = !loading && conversations.length === 0;\n\n return (\n <PageLayout\n title=\"Messages\"\n description=\"Your conversations with property owners, agents, and service providers\"\n purpose=\"Every thread you start from a property, listing, or service lands here — read the full history, pick up where you left off, and reply. Messages are delivered to the other party's inbox in real time.\"\n >\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <StatCard label=\"Conversations\" value={conversations.length} icon={<MessageSquare className=\"h-4 w-4\" />} />\n <StatCard label=\"Unread threads\" value={unreadThreads} icon={<MessageSquare className=\"h-4 w-4\" />} />\n <StatCard\n label=\"With updates today\"\n value={conversations.filter((c) => c.lastMessageAt && getRelativeTime(c.lastMessageAt) === 'Today').length}\n icon={<Send className=\"h-4 w-4\" />}\n />\n </div>\n </EmphasisPanel>\n\n {emptyInbox ? (\n <EmptyState\n illustration=\"empty-generic\"\n icon={<MessageSquare className=\"h-12 w-12\" />}\n title=\"No messages yet\"\n description=\"Start a conversation from any property or service — reach out to an owner or provider and your thread will show up here.\"\n action={<Button onClick={() => navigate('../properties')}>Browse properties</Button>}\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0\">\n <div className=\"grid h-[68vh] min-h-[26rem] grid-cols-1 lg:grid-cols-[20rem_1fr]\">\n {/* Thread list */}\n <aside\n className={cn('flex flex-col border-border-subtle lg:border-r', selectedId ? 'hidden lg:flex' : 'flex')}\n >\n <div className=\"border-b border-border-subtle p-3\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder=\"Search conversations\"\n aria-label=\"Search conversations\"\n className=\"h-10 w-full rounded-md border border-border-subtle bg-bg-surface pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {loading && conversations.length === 0 ? (\n <p className=\"p-6 text-center text-sm text-text-secondary\">Loading conversations…</p>\n ) : filtered.length === 0 ? (\n <p className=\"p-6 text-center text-sm text-text-secondary\">No conversations match your search.</p>\n ) : (\n <ul>\n {filtered.map((conversation) => {\n const active = conversation.id === selectedId;\n return (\n <li key={conversation.id}>\n <button\n type=\"button\"\n onClick={() => {\n setSelectedId(conversation.id);\n setDraft('');\n }}\n className={cn(\n 'flex w-full items-start gap-3 border-b border-border-subtle px-3 py-3 text-left transition-colors',\n active ? 'bg-action-primary-bg/10' : 'hover:bg-bg-sunken'\n )}\n >\n <Avatar title={conversation.title} id={conversation.id} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-sm font-semibold text-text-primary\">\n {conversation.title}\n </span>\n {conversation.lastMessageAt && (\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {getRelativeTime(conversation.lastMessageAt)}\n </span>\n )}\n </div>\n <div className=\"mt-0.5 flex items-center gap-2\">\n <span className=\"min-w-0 flex-1 truncate text-xs text-text-secondary\">\n {conversation.lastMessageText || 'No messages yet'}\n </span>\n {conversation.unreadCount > 0 && (\n <span className=\"inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full bg-action-primary-bg px-1.5 text-[0.65rem] font-semibold text-action-primary-text\">\n {conversation.unreadCount}\n </span>\n )}\n </div>\n </div>\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </aside>\n\n {/* Thread */}\n <section className={cn('flex min-h-0 flex-col', selectedId ? 'flex' : 'hidden lg:flex')}>\n {selected ? (\n <>\n <header className=\"flex items-center gap-3 border-b border-border-subtle p-3\">\n <button\n type=\"button\"\n onClick={() => setSelectedId(undefined)}\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken lg:hidden\"\n aria-label=\"Back to conversations\"\n >\n <ArrowLeft className=\"h-5 w-5\" />\n </button>\n <Avatar title={selected.title} id={selected.id} size=\"sm\" />\n <div className=\"min-w-0 flex-1\">\n <h2 className=\"truncate text-sm font-semibold text-text-primary\">{selected.title}</h2>\n {selected.contextHref && (\n <button\n type=\"button\"\n onClick={() => navigate(`..${selected.contextHref}`)}\n className=\"flex items-center gap-1 text-xs text-text-secondary transition-colors hover:text-primary\"\n >\n <MapPin className=\"h-3 w-3\" />\n View listing\n </button>\n )}\n </div>\n </header>\n\n <div ref={scrollRef} className=\"min-h-0 flex-1 space-y-2 overflow-y-auto bg-bg-sunken/40 p-4\">\n {loadingMessages && messages.length === 0 ? (\n <p className=\"py-10 text-center text-sm text-text-secondary\">Loading messages…</p>\n ) : messages.length === 0 ? (\n <p className=\"py-10 text-center text-sm text-text-secondary\">\n No messages yet. Say hello to get the conversation started.\n </p>\n ) : (\n messages.map((message) => {\n const mine = message.authorId === currentUserId;\n return (\n <div key={message.id} className={cn('flex', mine ? 'justify-end' : 'justify-start')}>\n <div className={cn('max-w-[78%]', mine ? 'items-end' : 'items-start')}>\n <div\n className={cn(\n 'rounded-2xl px-3.5 py-2 text-sm',\n mine\n ? 'rounded-br-sm bg-action-primary-bg text-action-primary-text'\n : 'rounded-bl-sm border border-border-subtle bg-bg-surface text-text-primary'\n )}\n >\n {message.text}\n </div>\n <div className={cn('mt-1 px-1 text-[0.65rem] text-text-secondary', mine && 'text-right')}>\n {messageTime(message.createdAt)}\n </div>\n </div>\n </div>\n );\n })\n )}\n </div>\n\n <div className=\"flex items-end gap-2 border-t border-border-subtle p-3\">\n <textarea\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n void handleSend();\n }\n }}\n rows={1}\n placeholder=\"Type a message…\"\n aria-label=\"Message\"\n className=\"max-h-32 min-h-10 flex-1 resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <Button type=\"button\" onClick={handleSend} disabled={!draft.trim() || sending}>\n <Send className=\"h-4 w-4\" />\n <span className=\"ml-1 hidden sm:inline\">Send</span>\n </Button>\n </div>\n </>\n ) : (\n <div className=\"hidden flex-1 items-center justify-center p-8 lg:flex\">\n <div className=\"text-center\">\n <MessageSquare className=\"mx-auto h-10 w-10 text-text-secondary\" />\n <p className=\"mt-3 text-sm text-text-secondary\">Select a conversation to read and reply.</p>\n </div>\n </div>\n )}\n </section>\n </div>\n </GlassCard>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatPage.js","names":[],"sources":["../../../../src/housingvista/pages/chat/ChatPage.tsx"],"sourcesContent":["/**\n * Messages — the real HousingVista inbox. A two-pane conversations UI (thread\n * list + chat thread with bubbles) backed by wspace-conversations-svc:\n * - `useHousingVistaConversations` lists the user's product threads,\n * - `useConversationMessages` streams the selected thread (author name +\n * attachments included),\n * - `useSendMessage` / `useSendContextMessage` post new messages, the latter\n * when the draft carries uploaded files.\n * Everything is persisted server-side; when there are no threads yet we show a\n * tasteful empty state (never fabricated rows). Fully responsive: on mobile the\n * list and the open thread swap places (back button), on desktop they sit\n * side-by-side.\n * @module housingvista\n */\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useNavigate } from 'react-router';\nimport { ArrowLeft, FileText, Loader2, MapPin, MessageSquare, Paperclip, Search, Send, X } from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState, StatCard } from '../../components';\nimport { useHousingVistaConversations, type HvConversationSummary } from '../../hooks/messages';\nimport {\n useConversationMessages,\n useFileUpload,\n useProperty,\n useSendContextMessage,\n useSendMessage,\n type HvMessageAttachment,\n type HvUploadedFile,\n} from '../../hooks';\nimport { useHousingVista } from '../../context/HousingVistaContext';\nimport { notify } from '../../lib/toast';\nimport { cn, formatCompactCurrency, getRelativeTime } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\n/** Deterministic accent for an avatar chip, derived from the thread title. */\nconst AVATAR_ACCENTS = [\n 'bg-status-info-bg-subtle text-status-info-text',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-action-primary-bg/15 text-primary',\n];\n\nfunction initials(title: string): string {\n const parts = title.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\nfunction accentFor(id: string): string {\n let sum = 0;\n for (let i = 0; i < id.length; i += 1) sum += id.charCodeAt(i);\n return AVATAR_ACCENTS[sum % AVATAR_ACCENTS.length];\n}\n\nfunction messageTime(iso: string): string {\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '';\n return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Human label for the thing a thread is about. The keys are the `entityType`\n * values this MFE stamps on a conversation when it opens one (see the\n * `ContactOwnerDialog` call sites); anything else falls through to the raw\n * value with its `housingvista-` prefix stripped, so an unknown type still\n * reads as words rather than a slug.\n */\nconst ENTITY_TYPE_LABELS: Record<string, string> = {\n 'housingvista-property': 'Property',\n 'housingvista-marketplace-service': 'Marketplace service',\n 'housingvista-legal-service': 'Legal service',\n 'housingvista-home-loan': 'Home loan',\n};\n\nfunction entityTypeLabel(entityType: string | null): string | null {\n if (!entityType) return null;\n const known = ENTITY_TYPE_LABELS[entityType];\n if (known) return known;\n const words = entityType\n .replace(/^housingvista-/, '')\n .replace(/[-_]/g, ' ')\n .trim();\n if (!words) return null;\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\n/** Byte count in the units a person reads (matches the mockup's file chips). */\nfunction formatFileSize(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes <= 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/**\n * `Conversation.avatar` is documented as \"URL or fileId\" — only the URL form is\n * renderable, so anything else falls back to the initials chip instead of a\n * broken image.\n */\nfunction avatarSrc(avatar: string | null | undefined): string | null {\n if (!avatar) return null;\n return /^(https?:|data:image\\/|\\/)/.test(avatar) ? avatar : null;\n}\n\nfunction Avatar({\n title,\n id,\n size = 'md',\n imageUrl,\n}: {\n title: string;\n id: string;\n size?: 'sm' | 'md';\n imageUrl?: string | null;\n}) {\n const sizing = size === 'md' ? 'h-10 w-10 text-sm' : 'h-9 w-9 text-xs';\n const src = avatarSrc(imageUrl);\n if (src) {\n return <img src={src} alt=\"\" aria-hidden className={cn('shrink-0 rounded-full object-cover', sizing)} />;\n }\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center rounded-full font-semibold',\n sizing,\n accentFor(id)\n )}\n aria-hidden\n >\n {initials(title)}\n </span>\n );\n}\n\n/** Attachment chips under a message bubble — a real download link per file. */\nfunction MessageAttachments({ attachments, mine }: { attachments: HvMessageAttachment[]; mine: boolean }) {\n if (attachments.length === 0) return null;\n return (\n <ul className={cn('mt-1.5 space-y-1', mine ? 'flex flex-col items-end' : '')}>\n {attachments.map((attachment) => {\n const size = formatFileSize(attachment.fileSize);\n const body = (\n <>\n <FileText className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <span className=\"truncate\">{attachment.fileName}</span>\n {size && <span className=\"shrink-0 text-xs text-text-secondary\">{size}</span>}\n </>\n );\n return (\n <li key={attachment.id} className=\"max-w-full\">\n {attachment.downloadUrl ? (\n <a\n href={attachment.downloadUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 rounded-md border border-border-subtle bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary hover:underline\"\n >\n {body}\n </a>\n ) : (\n <span\n className=\"flex items-center gap-2 rounded-md border border-border-subtle bg-bg-surface px-2.5 py-1.5 text-sm text-text-secondary\"\n title=\"This attachment has no download link\"\n >\n {body}\n </span>\n )}\n </li>\n );\n })}\n </ul>\n );\n}\n\nexport function ChatPage() {\n useTrackPageView('housingvista.chat.viewed');\n const navigate = useNavigate();\n const { currentUser } = useHousingVista();\n const currentUserId = currentUser?.id ?? '';\n\n const { data: conversations, loading, refetch } = useHousingVistaConversations();\n const { sendMessage, loading: sending } = useSendMessage();\n const { sendContextMessage, loading: sendingWithFiles } = useSendContextMessage();\n const { uploadFile } = useFileUpload();\n\n const [selectedId, setSelectedId] = useState<string | undefined>();\n const [query, setQuery] = useState('');\n const [draft, setDraft] = useState('');\n const [pendingFiles, setPendingFiles] = useState<HvUploadedFile[]>([]);\n const [uploading, setUploading] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Default-select the first thread on desktop once data lands.\n useEffect(() => {\n if (!selectedId && conversations.length > 0 && typeof window !== 'undefined' && window.innerWidth >= 1024) {\n setSelectedId(conversations[0].id);\n }\n }, [conversations, selectedId]);\n\n const selected: HvConversationSummary | undefined = useMemo(\n () => conversations.find((c) => c.id === selectedId),\n [conversations, selectedId]\n );\n\n const { data: messages, loading: loadingMessages } = useConversationMessages(selectedId);\n\n // When the open thread is about a property, read that property so the thread\n // header can carry its price the way the listing itself does. `entityId` is\n // the id the thread was stamped with, so this is the real listing — the query\n // is skipped for every other kind of thread.\n const propertyThreadId =\n selected?.entityType === 'housingvista-property' ? (selected.entityId ?? undefined) : undefined;\n const { data: threadProperty } = useProperty(propertyThreadId);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });\n }, [messages, selectedId]);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return conversations;\n return conversations.filter(\n (c) => c.title.toLowerCase().includes(q) || c.lastMessageText.toLowerCase().includes(q)\n );\n }, [conversations, query]);\n\n const unreadThreads = conversations.filter((c) => c.unreadCount > 0).length;\n\n // A thread can only take attachments when its persisted sourceContext is\n // complete — `sendContextMessage` is the only mutation that stores readable\n // attachment details, and it needs that context. No context, no paperclip:\n // the affordance is hidden rather than shown broken.\n const canAttach = Boolean(selected?.sourceContext);\n const busySending = sending || sendingWithFiles || uploading;\n\n const handleUploadFiles = async (fileList: FileList | null) => {\n if (!fileList || fileList.length === 0) return;\n setUploading(true);\n try {\n const uploaded: HvUploadedFile[] = [];\n for (const file of Array.from(fileList)) {\n uploaded.push(await uploadFile(file));\n }\n setPendingFiles((prev) => [...prev, ...uploaded]);\n } catch (err) {\n notify.error('Upload failed', (err as Error).message);\n } finally {\n setUploading(false);\n if (fileInputRef.current) fileInputRef.current.value = '';\n }\n };\n\n const handleSend = async () => {\n const text = draft.trim();\n if (!selectedId) return;\n if (!text && pendingFiles.length === 0) return;\n try {\n if (pendingFiles.length > 0 && selected?.sourceContext) {\n await sendContextMessage({ sourceContext: selected.sourceContext, text, attachments: pendingFiles });\n } else {\n if (!text) return;\n await sendMessage(selectedId, text);\n }\n setDraft('');\n setPendingFiles([]);\n void refetch();\n } catch (err) {\n notify.error('Message not sent', (err as Error).message);\n }\n };\n\n const emptyInbox = !loading && conversations.length === 0;\n\n return (\n <PageLayout\n title=\"Messages\"\n description=\"Your conversations with property owners, agents, and service providers\"\n purpose=\"Every thread you start from a property, listing, or service lands here — read the full history, pick up where you left off, and reply. Messages are delivered to the other party's inbox in real time.\"\n >\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <StatCard label=\"Conversations\" value={conversations.length} icon={<MessageSquare className=\"h-4 w-4\" />} />\n <StatCard label=\"Unread threads\" value={unreadThreads} icon={<MessageSquare className=\"h-4 w-4\" />} />\n <StatCard\n label=\"With updates today\"\n value={conversations.filter((c) => c.lastMessageAt && getRelativeTime(c.lastMessageAt) === 'Today').length}\n icon={<Send className=\"h-4 w-4\" />}\n />\n </div>\n </EmphasisPanel>\n\n {emptyInbox ? (\n <EmptyState\n illustration=\"empty-generic\"\n icon={<MessageSquare className=\"h-12 w-12\" />}\n title=\"No messages yet\"\n description=\"Start a conversation from any property or service — reach out to an owner or provider and your thread will show up here.\"\n action={<Button onClick={() => navigate('../properties')}>Browse properties</Button>}\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0\">\n <div className=\"grid h-[68vh] min-h-[26rem] grid-cols-1 lg:grid-cols-[20rem_1fr]\">\n {/* Thread list */}\n <aside\n className={cn('flex flex-col border-border-subtle lg:border-r', selectedId ? 'hidden lg:flex' : 'flex')}\n >\n <div className=\"border-b border-border-subtle p-3\">\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"search\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder=\"Search conversations\"\n aria-label=\"Search conversations\"\n className=\"h-10 w-full rounded-md border border-border-subtle bg-bg-surface pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n {loading && conversations.length === 0 ? (\n <p className=\"p-6 text-center text-sm text-text-secondary\">Loading conversations…</p>\n ) : filtered.length === 0 ? (\n <p className=\"p-6 text-center text-sm text-text-secondary\">No conversations match your search.</p>\n ) : (\n <ul>\n {filtered.map((conversation) => {\n const active = conversation.id === selectedId;\n return (\n <li key={conversation.id}>\n <button\n type=\"button\"\n onClick={() => {\n setSelectedId(conversation.id);\n setDraft('');\n setPendingFiles([]);\n }}\n className={cn(\n 'flex w-full items-start gap-3 border-b border-border-subtle px-3 py-3 text-left transition-colors',\n active ? 'bg-action-primary-bg/10' : 'hover:bg-bg-sunken'\n )}\n >\n <Avatar title={conversation.title} id={conversation.id} imageUrl={conversation.avatar} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-sm font-semibold text-text-primary\">\n {conversation.title}\n </span>\n {conversation.lastMessageAt && (\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {getRelativeTime(conversation.lastMessageAt)}\n </span>\n )}\n </div>\n {entityTypeLabel(conversation.entityType) && (\n <span className=\"mt-0.5 block truncate text-xs font-medium text-text-secondary\">\n {entityTypeLabel(conversation.entityType)}\n </span>\n )}\n <div className=\"mt-0.5 flex items-center gap-2\">\n <span className=\"min-w-0 flex-1 truncate text-xs text-text-secondary\">\n {conversation.lastMessageText ||\n (conversation.lastMessageIsFile ? 'Attachment' : 'No messages yet')}\n </span>\n {conversation.unreadCount > 0 && (\n <span className=\"inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full bg-action-primary-bg px-1.5 text-[0.65rem] font-semibold text-action-primary-text\">\n {conversation.unreadCount}\n </span>\n )}\n </div>\n </div>\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </aside>\n\n {/* Thread */}\n <section className={cn('flex min-h-0 flex-col', selectedId ? 'flex' : 'hidden lg:flex')}>\n {selected ? (\n <>\n <header className=\"flex items-center gap-3 border-b border-border-subtle p-3\">\n <button\n type=\"button\"\n onClick={() => setSelectedId(undefined)}\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken lg:hidden\"\n aria-label=\"Back to conversations\"\n >\n <ArrowLeft className=\"h-5 w-5\" />\n </button>\n <Avatar title={selected.title} id={selected.id} size=\"sm\" imageUrl={selected.avatar} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex min-w-0 items-baseline gap-2\">\n <h2 className=\"truncate text-sm font-semibold text-text-primary\">{selected.title}</h2>\n {threadProperty && (\n <span className=\"shrink-0 text-sm font-semibold text-primary\">\n {formatCompactCurrency(threadProperty.price.amount, threadProperty.price.currency)}\n {threadProperty.price.type === 'rent' && threadProperty.price.period === 'monthly'\n ? '/mo'\n : ''}\n </span>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n {entityTypeLabel(selected.entityType) && (\n <span className=\"text-xs text-text-secondary\">{entityTypeLabel(selected.entityType)}</span>\n )}\n {selected.contextHref && (\n <button\n type=\"button\"\n onClick={() => navigate(`..${selected.contextHref}`)}\n className=\"flex items-center gap-1 text-xs text-text-secondary transition-colors hover:text-primary\"\n >\n <MapPin className=\"h-3 w-3\" />\n View listing\n </button>\n )}\n </div>\n </div>\n </header>\n\n <div ref={scrollRef} className=\"min-h-0 flex-1 space-y-2 overflow-y-auto bg-bg-sunken/40 p-4\">\n {loadingMessages && messages.length === 0 ? (\n <p className=\"py-10 text-center text-sm text-text-secondary\">Loading messages…</p>\n ) : messages.length === 0 ? (\n <p className=\"py-10 text-center text-sm text-text-secondary\">\n No messages yet. Say hello to get the conversation started.\n </p>\n ) : (\n messages.map((message) => {\n const mine = message.authorId === currentUserId;\n return (\n <div key={message.id} className={cn('flex', mine ? 'justify-end' : 'justify-start')}>\n <div className={cn('max-w-[78%]', mine ? 'items-end' : 'items-start')}>\n {!mine && message.authorName && (\n <div className=\"mb-0.5 px-1 text-xs font-medium text-text-secondary\">\n {message.authorName}\n </div>\n )}\n {message.text && (\n <div\n className={cn(\n 'rounded-2xl px-3.5 py-2 text-sm',\n mine\n ? 'rounded-br-sm bg-action-primary-bg text-action-primary-text'\n : 'rounded-bl-sm border border-border-subtle bg-bg-surface text-text-primary'\n )}\n >\n {message.text}\n </div>\n )}\n <MessageAttachments attachments={message.attachments} mine={mine} />\n <div className={cn('mt-1 px-1 text-[0.65rem] text-text-secondary', mine && 'text-right')}>\n {messageTime(message.createdAt)}\n </div>\n </div>\n </div>\n );\n })\n )}\n </div>\n\n <div className=\"border-t border-border-subtle p-3\">\n {pendingFiles.length > 0 && (\n <ul className=\"mb-2 flex flex-wrap gap-2\">\n {pendingFiles.map((file) => (\n <li\n key={file.id}\n className=\"flex max-w-full items-center gap-2 rounded-md border border-border-subtle bg-bg-surface px-2.5 py-1.5 text-sm text-text-primary\"\n >\n <FileText className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <span className=\"truncate\">{file.name}</span>\n {formatFileSize(file.size) && (\n <span className=\"shrink-0 text-xs text-text-secondary\">{formatFileSize(file.size)}</span>\n )}\n <button\n type=\"button\"\n onClick={() => setPendingFiles((prev) => prev.filter((f) => f.id !== file.id))}\n aria-label={`Remove ${file.name}`}\n className=\"shrink-0 text-text-secondary transition-colors hover:text-status-error-text\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n <div className=\"flex items-end gap-2\">\n <textarea\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n void handleSend();\n }\n }}\n rows={1}\n placeholder=\"Type a message…\"\n aria-label=\"Message\"\n className=\"max-h-32 min-h-10 flex-1 resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n {canAttach && (\n <>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className=\"hidden\"\n onChange={(event) => void handleUploadFiles(event.target.files)}\n />\n <button\n type=\"button\"\n onClick={() => fileInputRef.current?.click()}\n disabled={uploading}\n aria-label=\"Attach files\"\n className=\"rounded-md border border-border-subtle p-2.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary disabled:opacity-60\"\n >\n {uploading ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Paperclip className=\"h-4 w-4\" />\n )}\n </button>\n </>\n )}\n <Button\n type=\"button\"\n onClick={handleSend}\n disabled={(!draft.trim() && pendingFiles.length === 0) || busySending}\n >\n <Send className=\"h-4 w-4\" />\n <span className=\"ml-1 hidden sm:inline\">Send</span>\n </Button>\n </div>\n </div>\n </>\n ) : (\n <div className=\"hidden flex-1 items-center justify-center p-8 lg:flex\">\n <div className=\"text-center\">\n <MessageSquare className=\"mx-auto h-10 w-10 text-text-secondary\" />\n <p className=\"mt-3 text-sm text-text-secondary\">Select a conversation to read and reply.</p>\n </div>\n </div>\n )}\n </section>\n </div>\n </GlassCard>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PropertiesPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/PropertiesPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"PropertiesPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/PropertiesPage.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AA2CH,eAAO,MAAM,cAAc,mCAu9B1B,CAAC"}
|