diff --git a/src/app/invoice/[id]/page.tsx b/src/app/invoice/[id]/page.tsx index c0e2e88..3b42f62 100644 --- a/src/app/invoice/[id]/page.tsx +++ b/src/app/invoice/[id]/page.tsx @@ -38,6 +38,8 @@ import PresenceIndicators from "@/components/PresenceIndicators"; import CollaborationCursors from "@/components/CollaborationCursors"; import SplitCalculator from "@/components/SplitCalculator"; import InvoiceQR from "@/components/InvoiceQR"; +import InvoiceChat from "@/components/InvoiceChat"; +import PaymentExport from "@/components/PaymentExport"; import { getReminderForInvoice, cancelReminder, setReminder } from "@/lib/reminders"; import { sendWebhookIfConfigured } from "@/components/WebhookConfig"; import TxConfirmModal from "@/components/TxConfirmModal"; @@ -529,6 +531,14 @@ export default function InvoiceDetailPage({ params }: Props) { /> + {/* Invoice Chat */} + + {/* Recipients */}

Recipients

diff --git a/src/components/InvoiceChat.tsx b/src/components/InvoiceChat.tsx new file mode 100644 index 0000000..9e4e6c1 --- /dev/null +++ b/src/components/InvoiceChat.tsx @@ -0,0 +1,198 @@ +"use client"; + +import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; +import { truncateAddress } from "@stellar-split/sdk"; + +type Recipient = { address: string }; + +interface InvoiceChatMessage { + invoiceId: string; + sender: string; + text: string; + timestamp: number; +} + +interface Props { + invoiceId: string; + creator: string; + recipients: Recipient[]; + currentAddress: string | null; +} + +const STORAGE_PREFIX = "invoice-chat-"; + +function getStorageKey(invoiceId: string) { + return `${STORAGE_PREFIX}${invoiceId}`; +} + +function parseMessages(value: string | null): InvoiceChatMessage[] { + try { + const parsed = value ? JSON.parse(value) : []; + if (!Array.isArray(parsed)) return []; + return parsed + .filter( + (item): item is InvoiceChatMessage => + typeof item === "object" && + item !== null && + typeof item.invoiceId === "string" && + typeof item.sender === "string" && + typeof item.text === "string" && + typeof item.timestamp === "number" + ) + .sort((a, b) => a.timestamp - b.timestamp); + } catch { + return []; + } +} + +function loadMessages(invoiceId: string): InvoiceChatMessage[] { + if (typeof window === "undefined") return []; + return parseMessages(localStorage.getItem(getStorageKey(invoiceId))); +} + +function saveMessage(invoiceId: string, message: InvoiceChatMessage) { + const messages = loadMessages(invoiceId); + localStorage.setItem( + getStorageKey(invoiceId), + JSON.stringify([...messages, message]) + ); +} + +function formatDate(timestamp: number) { + return new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", + }).format(new Date(timestamp)); +} + +export default function InvoiceChat({ + invoiceId, + creator, + recipients, + currentAddress, +}: Props) { + const [messages, setMessages] = useState([]); + const [text, setText] = useState(""); + const [isAllowed, setIsAllowed] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + setMessages(loadMessages(invoiceId)); + }, [invoiceId]); + + useEffect(() => { + setIsAllowed( + Boolean( + currentAddress && + (currentAddress === creator || + recipients.some((recipient) => recipient.address === currentAddress)) + ) + ); + }, [currentAddress, creator, recipients]); + + useEffect(() => { + const handleStorage = (event: StorageEvent) => { + if (event.key === getStorageKey(invoiceId)) { + setMessages(loadMessages(invoiceId)); + } + }; + + window.addEventListener("storage", handleStorage); + return () => window.removeEventListener("storage", handleStorage); + }, [invoiceId]); + + const sortedMessages = useMemo( + () => [...messages].sort((a, b) => a.timestamp - b.timestamp), + [messages] + ); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + if (!currentAddress || !isAllowed) return; + + const trimmed = text.trim(); + if (!trimmed) return; + + const message: InvoiceChatMessage = { + invoiceId, + sender: currentAddress, + text: trimmed, + timestamp: Date.now(), + }; + + saveMessage(invoiceId, message); + setMessages((prev) => [...prev, message]); + setText(""); + inputRef.current?.focus(); + }; + + const placeholder = currentAddress + ? isAllowed + ? "Write a message to the invoice participants…" + : "Only invoice creator and recipients can post messages." + : "Connect your wallet to join the chat."; + + return ( +
+

Invoice Chat

+
+ {sortedMessages.length === 0 ? ( +
+ No messages yet. Start the conversation with the invoice creator or recipients. +
+ ) : ( +
    + {sortedMessages.map((message, index) => ( +
  • +
    + + {truncateAddress(message.sender)} + + + {formatDate(message.timestamp)} + +
    +

    {message.text}

    +
  • + ))} +
+ )} + +
+