@@ -20,6 +20,8 @@ interface NotificationOptions {
2020 toastXml ?: string ;
2121}
2222
23+ const handledWindowsToastXMLProtocolActionsForIds : string [ ] = [ ] ;
24+
2325// Track active notifications by ID, with metadata for thread-based dismissal
2426const activeNotifications = new Map < string , { notification : Notification ; threadId ?: string } > ( ) ;
2527
@@ -35,7 +37,7 @@ const activeNotifications = new Map<string, { notification: Notification; thread
3537 *
3638 * Uses path.resolve() to prevent directory traversal attacks.
3739 */
38- const validateIconPath = ( iconPath : string ) : string | null => {
40+ const validateIconPath = ( iconPath : string | undefined ) : string | null => {
3941 if ( ! iconPath ) {
4042 return null ;
4143 }
@@ -149,16 +151,27 @@ const displayNotification = (
149151 const notification = new Notification ( notifOptions ) ;
150152
151153 // Handle click event
152- // On Windows with toastXml + activationType="protocol", clicks are routed through
153- // the OS protocol handler (mailspring:// URLs) rather than Electron's Activated callback,
154- // so this event won't fire. We register it anyway as a fallback for non-toastXml
155- // notifications or if protocol activation fails.
156154 notification . on ( 'click' , ( ) => {
157- sendToAllWindows ( 'notification:clicked' , {
155+ const payload = {
158156 id : options . id ,
159157 threadId : options . threadId ,
160158 messageId : options . messageId ,
161- } ) ;
159+ } ;
160+ if ( process . platform === 'win32' ) {
161+ // On Windows with toastXml + activationType="protocol", clicks are routed through
162+ // the OS protocol handler (mailspring:// URLs) rather than Electron's Activated callback,
163+ // but this event still fires first. It seems worth handling it in case the protocol handler
164+ // fails, since it's a big of a fragile approach, but we need to wait and let the event arrive
165+ // from the second-instance that is launched to handle the URL.
166+ setTimeout ( ( ) => {
167+ if ( handledWindowsToastXMLProtocolActionsForIds . includes ( payload . id ) ) {
168+ return ;
169+ }
170+ sendToAllWindows ( 'notification:clicked' , payload ) ;
171+ } , 1500 ) ;
172+ } else {
173+ sendToAllWindows ( 'notification:clicked' , payload ) ;
174+ }
162175 } ) ;
163176
164177 // Handle close event
@@ -227,16 +240,19 @@ export function registerNotificationIPCHandlers(ipcMain: IpcMain) {
227240}
228241
229242export function handleWindowsToastXMLProtocolAction ( parts : UrlWithParsedQuery ) {
230- const windowsNotifEventArgs = {
243+ const payload = {
231244 id : parts . query . id as string ,
232245 threadId : parts . query . threadId as string ,
233246 messageId : parts . query . messageId as string ,
234247 } ;
235248
249+ handledWindowsToastXMLProtocolActionsForIds . unshift ( payload . id ) ;
250+ handledWindowsToastXMLProtocolActionsForIds . splice ( 10 ) ;
251+
236252 if ( parts . host === 'notification-click' ) {
237- sendToAllWindows ( 'notification:clicked' , windowsNotifEventArgs ) ;
253+ sendToAllWindows ( 'notification:clicked' , payload ) ;
238254 } else if ( parts . host === 'notification-action' ) {
239255 const actionIndex = parseInt ( parts . query . actionIndex as string , 10 ) ;
240- sendToAllWindows ( 'notification:action' , { ...windowsNotifEventArgs , actionIndex } ) ;
256+ sendToAllWindows ( 'notification:action' , { ...payload , actionIndex } ) ;
241257 }
242258}
0 commit comments