Add web client and containerized deployment stack
All checks were successful
CI / test (push) Successful in 19s
All checks were successful
CI / test (push) Successful in 19s
Web client: - Added React + TypeScript + Vite + Tailwind application in web/. - Implemented auth, chat list, chat messages, typing indicators, file uploads, and voice recording/playback. - Added typed API layer, Zustand stores, and realtime websocket hook integration. Containerization: - Added backend Dockerfile and project .dockerignore. - Added web multi-stage Dockerfile with nginx static hosting and API/WS reverse proxy. - Added full docker-compose stack with postgres, redis, minio, backend, worker, mailpit, and web. - Added MinIO bucket bootstrap init job and updated README with Docker quick-start.
This commit is contained in:
44
web/src/components/MessageList.tsx
Normal file
44
web/src/components/MessageList.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useMemo } from "react";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useChatStore } from "../store/chatStore";
|
||||
import { formatTime } from "../utils/format";
|
||||
|
||||
export function MessageList() {
|
||||
const me = useAuthStore((s) => s.me);
|
||||
const activeChatId = useChatStore((s) => s.activeChatId);
|
||||
const messagesByChat = useChatStore((s) => s.messagesByChat);
|
||||
const typingByChat = useChatStore((s) => s.typingByChat);
|
||||
|
||||
const messages = useMemo(() => {
|
||||
if (!activeChatId) {
|
||||
return [];
|
||||
}
|
||||
return messagesByChat[activeChatId] ?? [];
|
||||
}, [activeChatId, messagesByChat]);
|
||||
|
||||
if (!activeChatId) {
|
||||
return <div className="flex h-full items-center justify-center text-slate-400">Select a chat</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{messages.map((message) => (
|
||||
<div className={`mb-3 flex ${message.sender_id === me?.id ? "justify-end" : "justify-start"}`} key={message.id}>
|
||||
<div className="max-w-[80%] rounded-lg bg-slate-800 px-3 py-2">
|
||||
{message.type === "voice" && message.text ? (
|
||||
<audio controls src={message.text} />
|
||||
) : (
|
||||
<p className="whitespace-pre-wrap break-words">{message.text}</p>
|
||||
)}
|
||||
<p className="mt-1 text-right text-[11px] text-slate-400">{formatTime(message.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-4 pb-2 text-xs text-slate-400">
|
||||
{(typingByChat[activeChatId] ?? []).length > 0 ? "Someone is typing..." : ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user