Skip to content

Commit b5ae2e1

Browse files
committed
feat(notifications): add MeetingDetected / MeetingEnded variants
Scaffolds the notification plumbing for mic-activity meeting detection (landing in the next commit). Adds `MeetingDetected(String)` and `MeetingEnded(String)` enum variants alongside their settings toggles (`show_meeting_detected` / `show_meeting_ended`, default on), manager helpers, and the "Meeting detected — tap to start recording" / "Meeting ended — tap to stop recording" body text. Surfaces both in the About debug dropdown so they can be fired manually.
1 parent 1e08fbb commit b5ae2e1

5 files changed

Lines changed: 150 additions & 1 deletion

File tree

frontend/src-tauri/src/notifications/commands.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ pub enum DebugNotificationKind {
2828
MeetingReminder,
2929
SystemError,
3030
Test,
31+
MeetingDetected,
32+
MeetingEnded,
3133
}
3234

3335
/// Initialize the notification manager (called during app setup)
@@ -338,6 +340,13 @@ pub async fn debug_show_notification(
338340
None => Err(anyhow::anyhow!("Notification manager not initialized")),
339341
}
340342
}
343+
DebugNotificationKind::MeetingDetected => {
344+
show_meeting_detected_notification(&app, manager_state.inner(), "Zoom".to_string())
345+
.await
346+
}
347+
DebugNotificationKind::MeetingEnded => {
348+
show_meeting_ended_notification(&app, manager_state.inner(), "Zoom".to_string()).await
349+
}
341350
};
342351

343352
result.map_err(|e| format!("Failed to show debug notification: {}", e))
@@ -563,4 +572,69 @@ pub async fn show_meeting_reminder_notification(
563572
log_error!("Cannot show meeting reminder notification: manager not initialized");
564573
Ok(())
565574
}
575+
}
576+
577+
/// Show "Meeting detected" notification. Lazy-initializes the manager on
578+
/// first call, matching `show_recording_started_notification`.
579+
pub async fn show_meeting_detected_notification<R: Runtime>(
580+
app_handle: &tauri::AppHandle<R>,
581+
manager_state: &NotificationManagerState<R>,
582+
app_name: String,
583+
) -> Result<()> {
584+
log_info!("Attempting to show meeting-detected notification for: {}", app_name);
585+
586+
let manager_lock = manager_state.read().await;
587+
if let Some(manager) = manager_lock.as_ref() {
588+
return manager.show_meeting_detected(app_name).await;
589+
}
590+
drop(manager_lock);
591+
592+
match initialize_notification_manager(app_handle.clone()).await {
593+
Ok(manager) => {
594+
let mut state_lock = manager_state.write().await;
595+
*state_lock = Some(manager);
596+
drop(state_lock);
597+
let manager_lock = manager_state.read().await;
598+
match manager_lock.as_ref() {
599+
Some(manager) => manager.show_meeting_detected(app_name).await,
600+
None => Ok(()),
601+
}
602+
}
603+
Err(e) => {
604+
log_error!("Failed to init notification manager for meeting-detected: {}", e);
605+
Ok(())
606+
}
607+
}
608+
}
609+
610+
/// Show "Meeting ended" notification. Same pattern as meeting-detected.
611+
pub async fn show_meeting_ended_notification<R: Runtime>(
612+
app_handle: &tauri::AppHandle<R>,
613+
manager_state: &NotificationManagerState<R>,
614+
app_name: String,
615+
) -> Result<()> {
616+
log_info!("Attempting to show meeting-ended notification for: {}", app_name);
617+
618+
let manager_lock = manager_state.read().await;
619+
if let Some(manager) = manager_lock.as_ref() {
620+
return manager.show_meeting_ended(app_name).await;
621+
}
622+
drop(manager_lock);
623+
624+
match initialize_notification_manager(app_handle.clone()).await {
625+
Ok(manager) => {
626+
let mut state_lock = manager_state.write().await;
627+
*state_lock = Some(manager);
628+
drop(state_lock);
629+
let manager_lock = manager_state.read().await;
630+
match manager_lock.as_ref() {
631+
Some(manager) => manager.show_meeting_ended(app_name).await,
632+
None => Ok(()),
633+
}
634+
}
635+
Err(e) => {
636+
log_error!("Failed to init notification manager for meeting-ended: {}", e);
637+
Ok(())
638+
}
639+
}
566640
}

frontend/src-tauri/src/notifications/manager.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,28 @@ impl<R: Runtime> NotificationManager<R> {
189189
self.system_handler.show_notification(notification).await
190190
}
191191

192+
/// Show a "Meeting detected" notification (mic-activity detection).
193+
pub async fn show_meeting_detected(&self, app_name: String) -> Result<()> {
194+
let settings = self.settings.read().await;
195+
if !settings.notification_preferences.show_meeting_detected {
196+
return Ok(());
197+
}
198+
drop(settings);
199+
let notification = Notification::meeting_detected(app_name);
200+
self.show_notification(notification).await
201+
}
202+
203+
/// Show a "Meeting ended" notification (mic-activity detection).
204+
pub async fn show_meeting_ended(&self, app_name: String) -> Result<()> {
205+
let settings = self.settings.read().await;
206+
if !settings.notification_preferences.show_meeting_ended {
207+
return Ok(());
208+
}
209+
drop(settings);
210+
let notification = Notification::meeting_ended(app_name);
211+
self.show_notification(notification).await
212+
}
213+
192214
/// Get current notification settings
193215
pub async fn get_settings(&self) -> NotificationSettings {
194216
self.settings.read().await.clone()
@@ -301,6 +323,8 @@ impl<R: Runtime> NotificationManager<R> {
301323
NotificationType::MeetingReminder(_) => settings.notification_preferences.show_meeting_reminders,
302324
NotificationType::SystemError(_) => settings.notification_preferences.show_system_errors,
303325
NotificationType::Test => true, // Always show test notifications
326+
NotificationType::MeetingDetected(_) => settings.notification_preferences.show_meeting_detected,
327+
NotificationType::MeetingEnded(_) => settings.notification_preferences.show_meeting_ended,
304328
}
305329
}
306330

frontend/src-tauri/src/notifications/settings.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ pub struct NotificationPreferences {
6060

6161
/// Minutes before meeting to show reminder (0 = disabled)
6262
pub meeting_reminder_minutes: Vec<u64>,
63+
64+
/// Show "Meeting detected" banner when mic-activity detection fires.
65+
#[serde(default = "default_true")]
66+
pub show_meeting_detected: bool,
67+
68+
/// Show "Meeting ended" banner when mic-activity detection fires
69+
/// during an active recording.
70+
#[serde(default = "default_true")]
71+
pub show_meeting_ended: bool,
72+
}
73+
74+
fn default_true() -> bool {
75+
true
6376
}
6477

6578
impl Default for NotificationSettings {
@@ -89,6 +102,8 @@ impl Default for NotificationPreferences {
89102
show_meeting_reminders: true,
90103
show_system_errors: true,
91104
meeting_reminder_minutes: vec![15, 5], // 15 minutes and 5 minutes before
105+
show_meeting_detected: true,
106+
show_meeting_ended: true,
92107
}
93108
}
94109
}

frontend/src-tauri/src/notifications/types.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ pub enum NotificationType {
2323
MeetingReminder(u64), // Duration in minutes
2424
SystemError(String),
2525
Test, // For testing notifications
26+
/// Auto-detected meeting start — a non-Meetily app has held the mic
27+
/// past the sustain threshold. Payload is the detected app's display
28+
/// name (e.g. "Zoom", "a browser meeting", "a meeting").
29+
MeetingDetected(String),
30+
/// Auto-detected meeting end — the previously-detected app released
31+
/// the mic for the full end-silence window while Meetily was
32+
/// recording. Payload is the detected app's display name.
33+
MeetingEnded(String),
2634
}
2735

2836
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -196,4 +204,28 @@ impl Notification {
196204
.with_priority(NotificationPriority::Normal)
197205
.with_timeout(NotificationTimeout::Seconds(5))
198206
}
207+
208+
pub fn meeting_detected(app_name: impl Into<String>) -> Self {
209+
let app = app_name.into();
210+
let body = if app == "a meeting" || app == "a browser meeting" {
211+
format!("Meeting detected — tap to start recording")
212+
} else {
213+
format!("Meeting detected — {}", app)
214+
};
215+
216+
Notification::new("Meetily", body, NotificationType::MeetingDetected(app))
217+
.with_priority(NotificationPriority::High)
218+
.with_timeout(NotificationTimeout::Seconds(10))
219+
}
220+
221+
pub fn meeting_ended(app_name: impl Into<String>) -> Self {
222+
let app = app_name.into();
223+
Notification::new(
224+
"Meetily",
225+
"Meeting ended — tap to stop recording",
226+
NotificationType::MeetingEnded(app),
227+
)
228+
.with_priority(NotificationPriority::High)
229+
.with_timeout(NotificationTimeout::Seconds(10))
230+
}
199231
}

frontend/src/components/About.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ type DebugNotificationKind =
2222
| 'transcription_complete'
2323
| 'meeting_reminder'
2424
| 'system_error'
25-
| 'test';
25+
| 'test'
26+
| 'meeting_detected'
27+
| 'meeting_ended';
2628

2729
// `prefKey` matches a Rust NotificationPreferences field; omitted when a type isn't gated by preference.
2830
const DEBUG_NOTIFICATION_ITEMS: Array<{
@@ -38,6 +40,8 @@ const DEBUG_NOTIFICATION_ITEMS: Array<{
3840
{ kind: 'meeting_reminder', label: 'Meeting reminder (5 min)', prefKey: 'show_meeting_reminders' },
3941
{ kind: 'system_error', label: 'System error', prefKey: 'show_system_errors' },
4042
{ kind: 'test', label: 'Generic test notification' },
43+
{ kind: 'meeting_detected', label: 'Meeting detected (auto)', prefKey: 'show_meeting_detected' },
44+
{ kind: 'meeting_ended', label: 'Meeting ended (auto)', prefKey: 'show_meeting_ended' },
4145
];
4246

4347

0 commit comments

Comments
 (0)