feat(meetings): live invite, scheduling calendar, Discord-style redesign

- Live invite: ring a clinic member into a room (socket call:invite + a bell
  notification); the invitee gets a toast with a Join action. Notifications of
  type "meeting" deep-link to /messages/meetings?room=, which auto-joins.
- Scheduling: new scheduled_meetings table + /api/meetings/events (list mine,
  create, delete); a Calendar tab on the Meetings page with a month picker
  (meeting-day dots), the day's agenda, and a Schedule-meeting dialog
  (title/date/time/participants).
- Redesign: rounded control bar with tooltips (mic/cam/screen/invite + separated
  red Leave), speaking ring on tiles (Web Audio), and live room-occupancy counts
  via call:presence broadcasts. Migration 0025.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-20 02:07:41 +03:00
parent e841cb56e1
commit 913a217e1d
16 changed files with 5046 additions and 133 deletions
+28 -1
View File
@@ -737,7 +737,34 @@
"startVideo": "Start video",
"stopVideo": "Stop video",
"shareScreen": "Share screen",
"leave": "Leave call"
"leave": "Leave call",
"calendar": "Calendar",
"calendarEmpty": "No meetings on this day.",
"invite": {
"add": "Add people",
"title": "Invite to call",
"description": "Ring a team member into {{room}}.",
"search": "Search team…",
"noMembers": "No other members.",
"ring": "Ring",
"invited": "Invited",
"join": "Join",
"toastTitle": "{{name}} invited you to a call"
},
"schedule": {
"cta": "Schedule meeting",
"title": "Schedule a meeting",
"description": "Plan a meeting and invite your team.",
"titleLabel": "Title",
"titlePlaceholder": "e.g. Weekly sync",
"date": "Date",
"time": "Time",
"participants": "Participants",
"save": "Schedule",
"saving": "Scheduling…",
"failedTitle": "Couldn't schedule meeting",
"failedBody": "Please try again."
}
},
"messages": {
"inbox": "Inbox",
+34
View File
@@ -35,3 +35,37 @@ export type CallPeer = {
userId: string;
userName: string;
};
// --- Scheduled meetings (calendar) -----------------------------------------
export type ScheduledMeeting = {
id: string;
title: string;
date: string; // YYYY-MM-DD
time: string; // HH:mm
participants: string[];
participantNames: string[];
createdBy: string | null;
};
export function listMeetingEvents(): Promise<ScheduledMeeting[]> {
return apiFetch<ScheduledMeeting[]>("/api/meetings/events");
}
export function createMeetingEvent(input: {
title: string;
date: string;
time: string;
participants: string[];
}): Promise<ScheduledMeeting> {
return apiFetch<ScheduledMeeting>("/api/meetings/events", {
method: "POST",
body: JSON.stringify(input),
});
}
export function deleteMeetingEvent(id: string): Promise<void> {
return apiFetch<void>(`/api/meetings/events/${encodeURIComponent(id)}`, {
method: "DELETE",
});
}
+2
View File
@@ -39,6 +39,8 @@ export function notificationHref(n: Notification): string | null {
switch (n.entityType) {
case "conversation":
return `/messages?conversation=${encodeURIComponent(n.entityId)}`;
case "meeting":
return `/messages/meetings?room=${encodeURIComponent(n.entityId)}`;
case "patient":
return `/patients?file=${encodeURIComponent(n.entityId)}`;
default: