From 02c9d0b3bd01e3273fc7b4113332754cc03910f3 Mon Sep 17 00:00:00 2001 From: highperfocused Date: Sun, 20 Apr 2025 22:44:06 +0200 Subject: [PATCH] feat: implement chat interface with message input and list components --- app/chat/[pubkey]/page.tsx | 23 +++++++++ components/chat/ChatHeader.tsx | 50 ++++++++++++++++++++ components/chat/ChatInterface.tsx | 65 ++++++++++++++++++++++++++ components/chat/MessageInput.tsx | 78 +++++++++++++++++++++++++++++++ components/chat/MessageList.tsx | 56 ++++++++++++++++++++++ 5 files changed, 272 insertions(+) create mode 100644 app/chat/[pubkey]/page.tsx create mode 100644 components/chat/ChatHeader.tsx create mode 100644 components/chat/ChatInterface.tsx create mode 100644 components/chat/MessageInput.tsx create mode 100644 components/chat/MessageList.tsx diff --git a/app/chat/[pubkey]/page.tsx b/app/chat/[pubkey]/page.tsx new file mode 100644 index 0000000..9a88fe1 --- /dev/null +++ b/app/chat/[pubkey]/page.tsx @@ -0,0 +1,23 @@ +import { Metadata } from 'next'; +import ChatInterface from '@/components/chat/ChatInterface'; + +export const metadata: Metadata = { + title: 'Chat | Lumina', + description: 'Direct message chat with a Nostr user', +}; + +interface ChatPageProps { + params: { + pubkey: string; + } +} + +export default function ChatPage({ params }: ChatPageProps) { + const { pubkey } = params; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/components/chat/ChatHeader.tsx b/components/chat/ChatHeader.tsx new file mode 100644 index 0000000..acb4d94 --- /dev/null +++ b/components/chat/ChatHeader.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useProfile } from 'nostr-react'; +import { ChevronLeft } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +interface ChatHeaderProps { + pubkey: string; +} + +export default function ChatHeader({ pubkey }: ChatHeaderProps) { + const router = useRouter(); + const { data: userData } = useProfile({ + pubkey, + }); + + const [displayName, setDisplayName] = useState(pubkey.substring(0, 8) + '...'); + + useEffect(() => { + if (userData?.name) { + setDisplayName(userData.name); + } + }, [userData]); + + return ( +
+ + + + + {displayName.substring(0, 2).toUpperCase()} + + +
+

{displayName}

+ {userData?.nip05 && ( +

{userData.nip05}

+ )} +
+
+ ); +} \ No newline at end of file diff --git a/components/chat/ChatInterface.tsx b/components/chat/ChatInterface.tsx new file mode 100644 index 0000000..d17eb1d --- /dev/null +++ b/components/chat/ChatInterface.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useNostrEvents, dateToUnix } from 'nostr-react'; +import ChatHeader from './ChatHeader'; +import MessageList from './MessageList'; +import MessageInput from './MessageInput'; + +interface ChatInterfaceProps { + pubkey: string; +} + +export default function ChatInterface({ pubkey }: ChatInterfaceProps) { + const [messages, setMessages] = useState([]); + const now = new Date(); + + // Fetch direct messages (kind 4) between the current user and the specified pubkey + const { events } = useNostrEvents({ + filter: { + kinds: [4], // Kind 4 is direct messages in Nostr + authors: [/* would be populated with current user pubkey */], + since: dateToUnix(new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)), // Last 7 days + }, + }); + + // For demo purposes, let's add some dummy messages + useEffect(() => { + const dummyMessages = [ + { id: '1', sender: 'you', content: 'Hello there!', timestamp: new Date(now.getTime() - 3600000) }, + { id: '2', sender: pubkey, content: 'Hi! How are you?', timestamp: new Date(now.getTime() - 3500000) }, + { id: '3', sender: 'you', content: 'I\'m doing well, thanks for asking!', timestamp: new Date(now.getTime() - 3400000) }, + { id: '4', sender: pubkey, content: 'That\'s great to hear! What have you been up to?', timestamp: new Date(now.getTime() - 3300000) }, + { id: '5', sender: 'you', content: 'Just exploring the Nostr protocol and building some cool stuff with it.', timestamp: new Date(now.getTime() - 3200000) }, + { id: '6', sender: pubkey, content: 'That sounds interesting! I\'ve been hearing a lot about Nostr lately.', timestamp: new Date(now.getTime() - 3100000) }, + { id: '7', sender: 'you', content: 'Yeah, it\'s a fascinating protocol for decentralized social networking. Very simple yet powerful.', timestamp: new Date(now.getTime() - 3000000) }, + { id: '8', sender: pubkey, content: 'I should look into it more. Any resources you recommend?', timestamp: new Date(now.getTime() - 2900000) }, + { id: '9', sender: 'you', content: 'Check out the NIPs (Nostr Implementation Possibilities) on GitHub, and there are several good clients to try out.', timestamp: new Date(now.getTime() - 2800000) }, + { id: '10', sender: pubkey, content: 'Thanks for the tip! I\'ll definitely check those out.', timestamp: new Date(now.getTime() - 2700000) }, + ]; + + setMessages(dummyMessages); + }, [pubkey]); + + const handleSendMessage = (message: string) => { + if (!message.trim()) return; + + // In a real implementation, this would publish to Nostr + const newMessage = { + id: Date.now().toString(), + sender: 'you', + content: message, + timestamp: new Date(), + }; + + setMessages(prev => [...prev, newMessage]); + }; + + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/components/chat/MessageInput.tsx b/components/chat/MessageInput.tsx new file mode 100644 index 0000000..78339d1 --- /dev/null +++ b/components/chat/MessageInput.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useState, useRef } from 'react'; +import { SendHorizonal, PaperclipIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface MessageInputProps { + onSendMessage: (message: string) => void; +} + +export default function MessageInput({ onSendMessage }: MessageInputProps) { + const [message, setMessage] = useState(''); + const textareaRef = useRef(null); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (message.trim()) { + onSendMessage(message); + setMessage(''); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Send on Enter (without shift for a new line) + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + // Auto-resize textarea based on content + const handleInput = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = 'auto'; + textarea.style.height = `${Math.min(textarea.scrollHeight, 150)}px`; + } + }; + + return ( +
+
+ + +
+