feat: wire mic-detect popup into note-taking flow

This commit is contained in:
Gagancreates 2026-05-15 14:41:18 +05:30 committed by Arjun
parent 6c9d9206c8
commit 8da40bd9bb
4 changed files with 73 additions and 29 deletions

View file

@ -53,7 +53,16 @@ export class MeetingDetectService {
this.pending.add(work);
void work.finally(() => this.pending.delete(work));
});
this.detector.on("meeting-cleared", (event) => {
// Mic released → drop the session's suppression so the next call
// (same Chrome process, new Meet) can fire again.
this.suppression.clearSession(event.sessionKey).catch((err) => {
console.error("[MeetingDetect] clearSession failed:", err);
});
console.log(`[MeetingDetect] session cleared: ${event.sessionKey}`);
});
this.detector.start();
console.log("[MeetingDetect] service started — polling for meeting apps holding the mic");
}
stop(): void {
@ -68,7 +77,11 @@ export class MeetingDetectService {
}
private async handleActive(event: MeetingActiveEvent): Promise<void> {
if (!this.suppression.shouldNotify(event.sessionKey, event.executable)) return;
console.log(`[MeetingDetect] active: ${event.executable} (kind=${event.kind})`);
if (!this.suppression.shouldNotify(event.sessionKey, event.executable)) {
console.log(`[MeetingDetect] suppressed (already notified or muted): ${event.sessionKey}`);
return;
}
// For browsers we MUST confirm the foreground tab is a meeting page —
// otherwise we'd popup for YouTube, Spotify web, etc.

View file

@ -93,6 +93,18 @@ export class Suppression {
await this.persist();
}
/**
* Clear the notified mark for a session. Called when the detector observes
* the mic being released without this, on Windows (no pid in sessionKey)
* the same browser would never re-fire because every new Meet call reuses
* the same exe-keyed session.
*/
async clearSession(sessionKey: string): Promise<void> {
if (!this.state.notifiedSessions[sessionKey]) return;
delete this.state.notifiedSessions[sessionKey];
await this.persist();
}
async markDismissed(executable: string, now: Date = new Date()): Promise<void> {
this.state.recentlyDismissed[dismissKeyFor(executable)] = { dismissedAt: now.toISOString() };
await this.persist();

View file

@ -3968,36 +3968,53 @@ function App() {
return window.ipc.on('app:openUrl', ({ url }) => handle(url))
}, [])
// Triggered by main when the user clicks a calendar-meeting notification.
// Reuses the same flow as the in-app "Join meeting & take notes" button.
// Triggered by main when the user clicks a meeting-notes notification —
// either the calendar-time notification (event populated) or the mic-detect
// ad-hoc notification (event=null, title=string). Both routes feed the same
// calendar-block flow which kicks off startMeetingNow().
// When `openMeeting` is true, also opens the meeting URL in the system browser.
useEffect(() => {
return window.ipc.on('app:takeMeetingNotes', ({ event, openMeeting }) => {
const e = event as {
summary?: string
start?: { dateTime?: string; date?: string; timeZone?: string }
end?: { dateTime?: string; date?: string; timeZone?: string }
location?: string
htmlLink?: string
hangoutLink?: string
conferenceData?: { entryPoints?: Array<{ entryPointType?: string; uri?: string }> }
}
if (!e || typeof e !== 'object') return
const conferenceLink = extractConferenceLink(e as Record<string, unknown>)
if (openMeeting && conferenceLink) {
window.open(conferenceLink, '_blank')
} else if (openMeeting) {
console.warn('[take-meeting-notes] openMeeting requested but event has no conference link', e)
}
window.__pendingCalendarEvent = {
summary: e.summary,
start: e.start,
end: e.end,
location: e.location,
htmlLink: e.htmlLink,
conferenceLink,
source: 'calendar-sync',
return window.ipc.on('app:takeMeetingNotes', ({ event, openMeeting, title }) => {
const payload = event as
| {
summary?: string
start?: { dateTime?: string; date?: string; timeZone?: string }
end?: { dateTime?: string; date?: string; timeZone?: string }
location?: string
htmlLink?: string
hangoutLink?: string
conferenceData?: { entryPoints?: Array<{ entryPointType?: string; uri?: string }> }
}
| null
| undefined
if (payload && typeof payload === 'object') {
const conferenceLink = extractConferenceLink(payload as Record<string, unknown>)
if (openMeeting && conferenceLink) {
window.open(conferenceLink, '_blank')
} else if (openMeeting) {
console.warn('[take-meeting-notes] openMeeting requested but event has no conference link', payload)
}
window.__pendingCalendarEvent = {
summary: payload.summary,
start: payload.start,
end: payload.end,
location: payload.location,
htmlLink: payload.htmlLink,
conferenceLink,
source: 'calendar-sync',
}
} else if (typeof title === 'string' && title.length > 0) {
// Ad-hoc detection — no calendar event matched. Build a minimal
// pending event from the title so the meeting flow can still start.
window.__pendingCalendarEvent = {
summary: title,
source: 'meeting-detect',
}
} else {
return
}
window.dispatchEvent(new Event('calendar-block:join-meeting'))
})
}, [])

View file

@ -395,11 +395,13 @@ const ipcSchemas = {
},
'app:takeMeetingNotes': {
req: z.object({
// Pass the raw calendar event JSON through; renderer adapts to its existing flow.
// Calendar event JSON when correlated; null for mic-detect ad-hoc fires.
event: z.unknown(),
// When true, the renderer should also open the meeting URL (Zoom/Meet/etc.)
// in addition to triggering the take-notes flow.
openMeeting: z.boolean().optional(),
// Fallback title for ad-hoc detection (no calendar event matched).
title: z.string().nullable().optional(),
}),
res: z.null(),
},