mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-09 19:45:17 +02:00
feat: wire mic-detect popup into note-taking flow
This commit is contained in:
parent
6c9d9206c8
commit
8da40bd9bb
4 changed files with 73 additions and 29 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
})
|
||||
}, [])
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue