@@ -17,6 +17,9 @@ interface Service {
1717 icon : string ;
1818 color : string ;
1919 notificationCount : number ;
20+ muted ?: boolean ;
21+ enabled ?: boolean ;
22+ notificationsEnabled ?: boolean ;
2023}
2124
2225interface StoreSchema {
@@ -41,6 +44,8 @@ let mainWindow: BrowserWindow | null = null;
4144const serviceViews = new Map < string , WebContentsView > ( ) ;
4245const notificationCounts = new Map < string , number > ( ) ;
4346let activeServiceId : string | null = null ;
47+ const pendingDecrease = new Map < string , { count : number ; streak : number } > ( ) ;
48+ const DECREASE_THRESHOLD = 3 ; // require 3 consecutive lower readings before decreasing
4449const SIDEBAR_WIDTH = 68 ;
4550const TITLEBAR_HEIGHT = 46 ;
4651
@@ -104,7 +109,7 @@ function createWindow() {
104109 mainWindow . webContents . on ( "did-finish-load" , ( ) => {
105110 const services = store . get ( "services" ) ;
106111 for ( const service of services ) {
107- if ( ! serviceViews . has ( service . id ) && mainWindow ) {
112+ if ( ! serviceViews . has ( service . id ) && mainWindow && service . enabled !== false ) {
108113 const view = createServiceView ( service ) ;
109114 serviceViews . set ( service . id , view ) ;
110115 mainWindow . contentView . addChildView ( view ) ;
@@ -188,18 +193,47 @@ function createServiceView(service: Service): WebContentsView {
188193
189194 view . webContents . loadURL ( service . url ) ;
190195
196+ // Apply mute state
197+ if ( service . muted ) {
198+ view . webContents . setAudioMuted ( true ) ;
199+ }
200+
191201 // Track page title changes for notification detection
202+ // Debounce decreases to avoid blinking badges during page transitions
192203 const updateNotificationCount = ( count : number ) => {
204+ // Check if notifications are disabled for this service
205+ const currentService = store . get ( "services" ) . find ( ( s ) => s . id === service . id ) ;
206+ if ( currentService ?. notificationsEnabled === false ) {
207+ count = 0 ;
208+ }
209+
193210 const prev = notificationCounts . get ( service . id ) || 0 ;
194- if ( count !== prev ) {
195- notificationCounts . set ( service . id , count ) ;
196- updateTaskbarBadge ( ) ;
197- if ( mainWindow ) {
198- mainWindow . webContents . send ( "notification-update" , {
199- serviceId : service . id ,
200- count,
201- } ) ;
211+ if ( count === prev ) {
212+ pendingDecrease . delete ( service . id ) ;
213+ return ;
214+ }
215+
216+ if ( count < prev ) {
217+ const pending = pendingDecrease . get ( service . id ) ;
218+ if ( pending && pending . count === count ) {
219+ pending . streak ++ ;
220+ if ( pending . streak < DECREASE_THRESHOLD ) return ;
221+ } else {
222+ pendingDecrease . set ( service . id , { count, streak : 1 } ) ;
223+ return ;
202224 }
225+ pendingDecrease . delete ( service . id ) ;
226+ } else {
227+ pendingDecrease . delete ( service . id ) ;
228+ }
229+
230+ notificationCounts . set ( service . id , count ) ;
231+ updateTaskbarBadge ( ) ;
232+ if ( mainWindow ) {
233+ mainWindow . webContents . send ( "notification-update" , {
234+ serviceId : service . id ,
235+ count,
236+ } ) ;
203237 }
204238 } ;
205239
@@ -211,7 +245,7 @@ function createServiceView(service: Service): WebContentsView {
211245
212246 // Poll for unread count (title-based + DOM-based for apps like WhatsApp)
213247 const pollInterval = setInterval ( ( ) => {
214- if ( view . webContents . isDestroyed ( ) ) {
248+ if ( ! view . webContents || view . webContents . isDestroyed ( ) ) {
215249 clearInterval ( pollInterval ) ;
216250 return ;
217251 }
@@ -223,20 +257,41 @@ function createServiceView(service: Service): WebContentsView {
223257 const titleMatch = document.title.match(/\\((\\d+)\\)/);
224258 if (titleMatch) return parseInt(titleMatch[1], 10);
225259
226- // WhatsApp: check the "Unread N" filter button or unread badge spans
260+ // Check for "Unread N" text in the page (e.g. WhatsApp filter button)
227261 const allText = document.body.innerText || "";
228- const unreadTabMatch = allText.match(/Unread\\s+(\\d+)/);
262+ const unreadTabMatch = allText.match(/Unread\\s+(\\d+)/i );
229263 if (unreadTabMatch) return parseInt(unreadTabMatch[1], 10);
230264
231- // WhatsApp: count green unread indicator dots in chat list
232- const unreadSpans = document.querySelectorAll('span [aria-label*="unread" ]');
233- if (unreadSpans.length > 0) {
234- let total = 0;
235- unreadSpans.forEach (el => {
265+ // Count elements with aria-label containing "unread" (case-insensitive)
266+ const allElements = document.querySelectorAll('[aria-label]');
267+ let unreadTotal = 0;
268+ allElements.forEach(el => {
269+ if (el.getAttribute('aria-label').toLowerCase().includes('unread')) {
236270 const num = parseInt(el.textContent || "0", 10);
237- total += num > 0 ? num : 1;
271+ unreadTotal += num > 0 ? num : 1;
272+ }
273+ });
274+ if (unreadTotal > 0) return unreadTotal;
275+
276+ // Messenger: count chat rows with unread delivery status indicators
277+ const messengerUnread = document.querySelectorAll('[data-testid="unread-indicator"], [aria-label*="Delivered"], [aria-label*="Sent"]');
278+ if (messengerUnread.length === 0) {
279+ // Fallback: count bold/unread chat previews in Messenger
280+ // Messenger marks unread chat names with heavier font weight
281+ const chatRows = document.querySelectorAll('[role="row"], [role="listitem"]');
282+ let boldCount = 0;
283+ chatRows.forEach(row => {
284+ const spans = row.querySelectorAll('span');
285+ spans.forEach(span => {
286+ const weight = window.getComputedStyle(span).fontWeight;
287+ if ((weight === 'bold' || parseInt(weight) >= 700) && span.textContent && span.textContent.trim().length > 0 && span.closest('[role="row"], [role="listitem"]') === row) {
288+ // Check if this row also has a small colored dot (unread indicator)
289+ const dots = row.querySelectorAll('span[data-visualcompletion="ignore"]');
290+ if (dots.length > 0) boldCount++;
291+ }
292+ });
238293 });
239- if (total > 0) return total ;
294+ if (boldCount > 0) return boldCount ;
240295 }
241296
242297 return 0;
@@ -305,7 +360,7 @@ function showService(serviceId: string) {
305360 if ( ! view ) {
306361 const services = store . get ( "services" ) ;
307362 const service = services . find ( ( s ) => s . id === serviceId ) ;
308- if ( ! service ) return ;
363+ if ( ! service || service . enabled === false ) return ;
309364 view = createServiceView ( service ) ;
310365 serviceViews . set ( serviceId , view ) ;
311366 mainWindow . contentView . addChildView ( view ) ;
@@ -376,6 +431,66 @@ ipcMain.handle("reorder-services", (_event, serviceIds: string[]) => {
376431 return reordered ;
377432} ) ;
378433
434+ ipcMain . handle ( "toggle-mute-service" , ( _event , serviceId : string ) => {
435+ const services = store . get ( "services" ) ;
436+ const updated = services . map ( ( s ) => {
437+ if ( s . id === serviceId ) {
438+ const muted = ! s . muted ;
439+ // Apply mute to the live view
440+ const view = serviceViews . get ( serviceId ) ;
441+ if ( view ) {
442+ view . webContents . setAudioMuted ( muted ) ;
443+ }
444+ return { ...s , muted } ;
445+ }
446+ return s ;
447+ } ) ;
448+ store . set ( "services" , updated ) ;
449+ return updated ;
450+ } ) ;
451+
452+ ipcMain . handle ( "toggle-service-enabled" , ( _event , serviceId : string ) => {
453+ const services = store . get ( "services" ) ;
454+ const updated = services . map ( ( s ) => {
455+ if ( s . id === serviceId ) {
456+ const enabled = s . enabled === false ; // toggle: undefined/true -> false, false -> true
457+ if ( ! enabled ) {
458+ // Destroy the view when disabling
459+ const view = serviceViews . get ( serviceId ) ;
460+ if ( view ) {
461+ if ( activeServiceId === serviceId ) {
462+ activeServiceId = null ;
463+ }
464+ if ( mainWindow ) {
465+ mainWindow . contentView . removeChildView ( view ) ;
466+ }
467+ view . webContents . close ( ) ;
468+ serviceViews . delete ( serviceId ) ;
469+ }
470+ notificationCounts . delete ( serviceId ) ;
471+ pendingDecrease . delete ( serviceId ) ;
472+ updateTaskbarBadge ( ) ;
473+ }
474+ return { ...s , enabled } ;
475+ }
476+ return s ;
477+ } ) ;
478+ store . set ( "services" , updated ) ;
479+ return updated ;
480+ } ) ;
481+
482+ ipcMain . handle ( "toggle-service-notifications" , ( _event , serviceId : string ) => {
483+ const services = store . get ( "services" ) ;
484+ const updated = services . map ( ( s ) => {
485+ if ( s . id === serviceId ) {
486+ return { ...s , notificationsEnabled : s . notificationsEnabled === false } ;
487+ }
488+ return s ;
489+ } ) ;
490+ store . set ( "services" , updated ) ;
491+ return updated ;
492+ } ) ;
493+
379494ipcMain . on ( "show-service" , ( _event , serviceId : string ) => {
380495 showService ( serviceId ) ;
381496} ) ;
0 commit comments