From b993b86f1e4a3f05e6bac137b4b1c6889b94a031 Mon Sep 17 00:00:00 2001 From: mariocodecr Date: Fri, 19 Jun 2026 17:45:46 -0600 Subject: [PATCH 1/7] feat(chat): add payment and request message types --- .../frontend/components/chat/types.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/p2p-safe-swap/frontend/components/chat/types.ts b/p2p-safe-swap/frontend/components/chat/types.ts index f6d4925..101ebc3 100644 --- a/p2p-safe-swap/frontend/components/chat/types.ts +++ b/p2p-safe-swap/frontend/components/chat/types.ts @@ -11,3 +11,24 @@ export interface TextMessage extends ChatMessageBase { text: string; deliveryStatus?: "sent" | "delivered" | "read"; } + +export type PaymentStatus = "pending" | "completed" | "rejected"; + +export interface PaymentMessage extends ChatMessageBase { + type: "payment"; + amount: number; + currency: string; + memo?: string; + status: PaymentStatus; + receiptUrl?: string; +} + +export interface PaymentRequestMessage extends ChatMessageBase { + type: "request"; + amount: number; + currency: string; + memo?: string; + status: PaymentStatus; +} + +export type ChatMessage = TextMessage | PaymentMessage | PaymentRequestMessage; From 4841c25104f4a393f9bd372b0dfb347833abac3f Mon Sep 17 00:00:00 2001 From: mariocodecr Date: Fri, 19 Jun 2026 17:45:57 -0600 Subject: [PATCH 2/7] feat(chat): add address and day grouping helpers --- .../frontend/components/chat/utils.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/p2p-safe-swap/frontend/components/chat/utils.ts b/p2p-safe-swap/frontend/components/chat/utils.ts index 3067732..80000fe 100644 --- a/p2p-safe-swap/frontend/components/chat/utils.ts +++ b/p2p-safe-swap/frontend/components/chat/utils.ts @@ -1,3 +1,5 @@ +import type { ChatMessage } from "./types"; + export function formatTime(timestamp: string | Date): string { const date = typeof timestamp === "string" ? new Date(timestamp) : timestamp; return new Intl.DateTimeFormat("es-ES", { @@ -6,3 +8,66 @@ export function formatTime(timestamp: string | Date): string { hour12: false, }).format(date); } + +export function truncateAddress(address: string): string { + if (address.length <= 12) return address; + return `${address.slice(0, 6)}…${address.slice(-4)}`; +} + +function startOfDay(date: Date): number { + const copy = new Date(date); + copy.setHours(0, 0, 0, 0); + return copy.getTime(); +} + +export function formatDayLabel(timestamp: string | Date): string { + const date = typeof timestamp === "string" ? new Date(timestamp) : timestamp; + const today = startOfDay(new Date()); + const target = startOfDay(date); + const dayMs = 24 * 60 * 60 * 1000; + + if (target === today) return "HOY"; + if (target === today - dayMs) return "AYER"; + + return new Intl.DateTimeFormat("es-ES", { + day: "2-digit", + month: "long", + }) + .format(date) + .toUpperCase(); +} + +export interface ChatMessageGroup { + dayKey: number; + dayLabel: string; + messages: ChatMessage[]; +} + +export function groupMessagesByDay(messages: ChatMessage[]): ChatMessageGroup[] { + const sorted = [...messages].sort((a, b) => { + const aTime = new Date(a.timestamp).getTime(); + const bTime = new Date(b.timestamp).getTime(); + return aTime - bTime; + }); + + const groups: ChatMessageGroup[] = []; + + for (const message of sorted) { + const date = new Date(message.timestamp); + const dayKey = startOfDay(date); + const last = groups[groups.length - 1]; + + if (last && last.dayKey === dayKey) { + last.messages.push(message); + continue; + } + + groups.push({ + dayKey, + dayLabel: formatDayLabel(date), + messages: [message], + }); + } + + return groups; +} From cc674ab11cf9331129c5d8ab9ecb98f3cb62375f Mon Sep 17 00:00:00 2001 From: mariocodecr Date: Fri, 19 Jun 2026 17:46:02 -0600 Subject: [PATCH 3/7] feat(chat): add ChatHeader with WalletBadge --- .../frontend/components/chat/chat-header.tsx | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 p2p-safe-swap/frontend/components/chat/chat-header.tsx diff --git a/p2p-safe-swap/frontend/components/chat/chat-header.tsx b/p2p-safe-swap/frontend/components/chat/chat-header.tsx new file mode 100644 index 0000000..82ea0bb --- /dev/null +++ b/p2p-safe-swap/frontend/components/chat/chat-header.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { cn } from "@/lib/utils"; +import { WalletBadge } from "@/frontend/components/ui/wallet-badge"; +import { truncateAddress } from "./utils"; + +export interface ChatHeaderProps { + counterpartAddress: string; + isOnline?: boolean; + onBack?: () => void; + onMore?: () => void; + className?: string; +} + +export function ChatHeader({ + counterpartAddress, + isOnline = true, + onBack, + onMore, + className, +}: ChatHeaderProps) { + const [copied, setCopied] = useState(false); + const truncated = truncateAddress(counterpartAddress); + + const handleCopy = useCallback(async () => { + if (typeof navigator === "undefined" || !navigator.clipboard) return; + try { + await navigator.clipboard.writeText(counterpartAddress); + setCopied(true); + window.setTimeout(() => setCopied(false), 1500); + } catch { + setCopied(false); + } + }, [counterpartAddress]); + + return ( +
+ {onBack ? ( + + ) : null} + + + +
+ + {truncated} + + + +
+ + + + +
+ ); +} From e4a6ec684d218f1dc08b985bb6599544092679de Mon Sep 17 00:00:00 2001 From: mariocodecr Date: Fri, 19 Jun 2026 17:46:06 -0600 Subject: [PATCH 4/7] feat(chat): add DateSeparator component --- .../components/chat/date-separator.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 p2p-safe-swap/frontend/components/chat/date-separator.tsx diff --git a/p2p-safe-swap/frontend/components/chat/date-separator.tsx b/p2p-safe-swap/frontend/components/chat/date-separator.tsx new file mode 100644 index 0000000..446e791 --- /dev/null +++ b/p2p-safe-swap/frontend/components/chat/date-separator.tsx @@ -0,0 +1,21 @@ +import { cn } from "@/lib/utils"; + +export interface DateSeparatorProps { + label: string; + className?: string; +} + +export function DateSeparator({ label, className }: DateSeparatorProps) { + return ( +
+ + {label} + +
+ ); +} From faf4671c7dbc986f37e75343df68e1b2995cc307 Mon Sep 17 00:00:00 2001 From: mariocodecr Date: Fri, 19 Jun 2026 17:46:11 -0600 Subject: [PATCH 5/7] feat(chat): add ChatInputBar using Button --- .../components/chat/chat-input-bar.tsx | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 p2p-safe-swap/frontend/components/chat/chat-input-bar.tsx diff --git a/p2p-safe-swap/frontend/components/chat/chat-input-bar.tsx b/p2p-safe-swap/frontend/components/chat/chat-input-bar.tsx new file mode 100644 index 0000000..01ed05b --- /dev/null +++ b/p2p-safe-swap/frontend/components/chat/chat-input-bar.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState, type FormEvent, type KeyboardEvent } from "react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/frontend/components/ui/Button/Button"; + +export interface ChatInputBarProps { + onSendMessage: (text: string) => void; + onSendPayment: () => void; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +export function ChatInputBar({ + onSendMessage, + onSendPayment, + placeholder = "Escribe un mensaje…", + disabled = false, + className, +}: ChatInputBarProps) { + const [text, setText] = useState(""); + + const submit = () => { + const trimmed = text.trim(); + if (!trimmed || disabled) return; + onSendMessage(trimmed); + setText(""); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + submit(); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + submit(); + } + }; + + const canSend = text.trim().length > 0 && !disabled; + + return ( +
+