mirror of
https://github.com/temetro/temetro.git
synced 2026-08-25 18:07:44 +00:00
feat: real-time staff messaging over Socket.io
Add conversations/participants/messages tables, a participant-scoped REST API (/api/conversations) and a Socket.io server (session-authenticated handshake; per-user + per-conversation rooms) sharing the HTTP port. New messages broadcast live and create per-recipient notifications. Also lands the notifications table + service + routes (used by the message flow). The Messages page is rewritten: live threads, unread state, and a compose dialog to start a conversation with a clinic member. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
|
||||
// Messaging shapes. Mirror the backend `src/types/messaging.ts`.
|
||||
export type Participant = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ConversationMessage = {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type ConversationSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
isGroup: boolean;
|
||||
participants: Participant[];
|
||||
lastMessage: ConversationMessage | null;
|
||||
unread: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function listConversations(): Promise<ConversationSummary[]> {
|
||||
return apiFetch<ConversationSummary[]>("/api/conversations");
|
||||
}
|
||||
|
||||
export function listClinicMembers(): Promise<Participant[]> {
|
||||
return apiFetch<Participant[]>("/api/conversations/members");
|
||||
}
|
||||
|
||||
export function createConversation(input: {
|
||||
participantIds: string[];
|
||||
name?: string | null;
|
||||
}): Promise<ConversationSummary> {
|
||||
return apiFetch<ConversationSummary>("/api/conversations", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function getMessages(
|
||||
conversationId: string,
|
||||
): Promise<ConversationMessage[]> {
|
||||
return apiFetch<ConversationMessage[]>(
|
||||
`/api/conversations/${conversationId}/messages`,
|
||||
);
|
||||
}
|
||||
|
||||
// REST fallback for sending (the realtime path is the socket "message:send").
|
||||
export function sendMessageRest(
|
||||
conversationId: string,
|
||||
body: string,
|
||||
): Promise<ConversationMessage> {
|
||||
return apiFetch<ConversationMessage>(
|
||||
`/api/conversations/${conversationId}/messages`,
|
||||
{ method: "POST", body: JSON.stringify({ body }) },
|
||||
);
|
||||
}
|
||||
|
||||
export function markConversationRead(conversationId: string): Promise<void> {
|
||||
return apiFetch<void>(`/api/conversations/${conversationId}/read`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
|
||||
import { API_BASE_URL } from "@/lib/api-client";
|
||||
|
||||
// A single shared Socket.io connection to the backend, authenticated by the
|
||||
// Better Auth session cookie (withCredentials). Used by messaging and
|
||||
// notifications. Created lazily on first use in the browser.
|
||||
let socket: Socket | null = null;
|
||||
|
||||
export function getSocket(): Socket {
|
||||
if (!socket) {
|
||||
socket = io(API_BASE_URL, {
|
||||
withCredentials: true,
|
||||
transports: ["websocket", "polling"],
|
||||
});
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
Reference in New Issue
Block a user