Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,4 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
TopNotify/.DS_Store
/TopNotify/dist/
23 changes: 23 additions & 0 deletions TopNotify/Common/AppReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@ public class AppReference
/// </summary>
public string SoundDisplayName;

public NotificationStickyMode StickyMode = NotificationStickyMode.Default;
public int StickyDurationSeconds = 5;

public NotificationStickyMode GetStickyMode(Settings settings)
{
if (StickyMode == NotificationStickyMode.Default)
{
return settings.StickyMode;
}

return StickyMode;
}

public int GetStickyDurationSeconds(Settings settings)
{
if (StickyMode == NotificationStickyMode.Default)
{
return settings.StickyDurationSeconds;
}

return StickyDurationSeconds;
}

/// <summary>
/// Identifies An AppReference Based On A Notification
/// </summary>
Expand Down
82 changes: 82 additions & 0 deletions TopNotify/Common/NotificationLifetimeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace TopNotify.Common
{
public class NotificationLifetimeController
{
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);

[DllImport("user32.dll", SetLastError = true)]
private static extern bool SystemParametersInfo(uint uiAction, uint uiParam, uint pvParam, uint fWinIni);

const uint HWND_BROADCAST = 0xffff;
const uint WM_SETTINGCHANGE = 0x001A;
const uint SMTO_ABORTIFHUNG = 0x0002;
const uint SPI_SETMESSAGEDURATION = 0x2017;
const uint SPIF_UPDATEINIFILE = 0x01;
const uint SPIF_SENDCHANGE = 0x02;
const int PermanentDurationSeconds = 86400;

public static void Apply(Settings settings)
{
var duration = GetWindowsDuration(settings);
if (duration == null) { return; }

try
{
SystemParametersInfo(SPI_SETMESSAGEDURATION, 0, (uint)duration.Value, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);

using var key = Registry.CurrentUser.CreateSubKey(@"Control Panel\Accessibility");
key.SetValue("MessageDuration", duration.Value, RegistryValueKind.DWord);
SendMessageTimeout(new IntPtr(HWND_BROADCAST), WM_SETTINGCHANGE, UIntPtr.Zero, @"Control Panel\Accessibility", SMTO_ABORTIFHUNG, 1000, out _);
Program.Logger?.Information($"Applied Windows notification duration: {duration.Value} seconds");
}
catch (Exception ex)
{
Program.Logger?.Warning($"Failed to update Windows notification duration: {ex.Message}");
}
}

public static int? GetWindowsDuration(Settings settings)
{
var durations = new List<int>();

AddDuration(durations, settings.StickyMode, settings.StickyDurationSeconds);

foreach (var appReference in settings.AppReferences)
{
var mode = appReference.GetStickyMode(settings);
var seconds = appReference.GetStickyDurationSeconds(settings);
AddDuration(durations, mode, seconds);
}

if (!durations.Any()) { return null; }

return durations.Max();
}

public static int GetPermanentDurationSeconds()
{
return PermanentDurationSeconds;
}

private static void AddDuration(List<int> durations, NotificationStickyMode mode, int seconds)
{
if (mode == NotificationStickyMode.Permanent)
{
durations.Add(PermanentDurationSeconds);
}
else if (mode == NotificationStickyMode.Seconds)
{
durations.Add(Math.Clamp(seconds, 1, PermanentDurationSeconds));
}
}
}
}
11 changes: 11 additions & 0 deletions TopNotify/Common/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public class Settings

public string PreferredMonitor = "primary";

public NotificationStickyMode StickyMode = NotificationStickyMode.WindowsDefault;
public int StickyDurationSeconds = 5;

public List<AppReference> AppReferences = new List<AppReference>();

// Dynamic Fields That Are Cached, Useful For Interop
Expand Down Expand Up @@ -163,4 +166,12 @@ public enum NotifyLocation
BottomRight,
Custom
}

public enum NotificationStickyMode
{
Default,
WindowsDefault,
Seconds,
Permanent
}
}
72 changes: 62 additions & 10 deletions TopNotify/Daemon/InterceptorManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ public class InterceptorManager
public const int ReflowTimeout = 50;

public ConcurrentDictionary<uint, Action?> CleanUpFunctions = new ConcurrentDictionary<uint, Action?>(); // Maps HandledNotifications to the associated clean up function
public ConcurrentDictionary<uint, bool> KnownNotificationIds = new ConcurrentDictionary<uint, bool>();
public UserNotificationListener Listener;
public bool CanListenToNotifications = false;
public bool IsPollingNotifications = false;
public DateTime LastNotificationPoll = DateTime.MinValue;

public static Interceptor[] InstalledInterceptors =
{
Expand All @@ -44,6 +47,7 @@ public void Start()
{
Instance = this;
CurrentSettings = Settings.Get();
NotificationLifetimeController.Apply(CurrentSettings);

foreach (var possibleInterceptor in InstalledInterceptors)
{
Expand All @@ -62,8 +66,7 @@ public void Start()
var access = await Listener.RequestAccessAsync();
if (access != UserNotificationListenerAccessStatus.Allowed)
{
var msg = "Failed To Start Notification Listener: Permission Denied";
DaemonErrorHandler.ThrowNonCritical(new DaemonError("listener_failure_no_permission", msg));
Program.Logger.Warning("Failed To Start Notification Listener: Permission Denied");
return;
}

Expand All @@ -76,12 +79,12 @@ public void Start()
}
catch (Exception ex)
{
var msg = "Failed To Start Notification Listener: Not Packaged";
DaemonErrorHandler.ThrowNonCritical(new DaemonError("listener_failure_not_packaged", msg));
Program.Logger.Warning("Failed To Start Notification Listener: Not Packaged");
return;
}

CanListenToNotifications = true;
PollNotifications();

});

Expand All @@ -105,6 +108,7 @@ public void MainLoop()
Reflow();
}

PollNotifications();
Update();

Thread.Sleep(10);
Expand Down Expand Up @@ -162,22 +166,70 @@ public async void OnNotificationChanged(UserNotificationListener sender, UserNot

if (args.ChangeKind == UserNotificationChangedKind.Added)
{
foreach (Interceptor i in Interceptors)
if (KnownNotificationIds.TryAdd(args.UserNotificationId, true))
{
try
{
i.OnNotification(userNotification);
}
catch { }
DispatchNotification(userNotification);
}
}
else
{
KnownNotificationIds.TryRemove(args.UserNotificationId, out _);
}

Update();
}

public void PollNotifications()
{
if (!CanListenToNotifications) { return; }
if (IsPollingNotifications) { return; }
if ((DateTime.Now - LastNotificationPoll).TotalMilliseconds < 500) { return; }

IsPollingNotifications = true;
LastNotificationPoll = DateTime.Now;

Task.Run(async () =>
{
try
{
var userNotifications = await Listener.GetNotificationsAsync(NotificationKinds.Toast);
foreach (var notification in userNotifications)
{
if (KnownNotificationIds.TryAdd(notification.Id, true))
{
DispatchNotification(notification);
}
}
}
catch (Exception ex)
{
Program.Logger?.Warning($"Failed to poll notifications: {ex.Message}");
}
finally
{
IsPollingNotifications = false;
}
});
}

public void DispatchNotification(UserNotification userNotification)
{
if (userNotification == null) { return; }

foreach (Interceptor i in Interceptors)
{
try
{
i.OnNotification(userNotification);
}
catch { }
}
}

public void OnSettingsChanged()
{
CurrentSettings = Settings.Get();
NotificationLifetimeController.Apply(CurrentSettings);

foreach (Interceptor i in Interceptors)
{
Expand Down
Loading