Skip to content

Commit

Permalink
Image support (#21)
Browse files Browse the repository at this point in the history
* Image support

* Fix invalid dismiss reason

* caps

* Test

* Cappabilities

* Fix dict caps

* Prevent images from getting used when unsupported

* Fix textbox

* Update README.md

* Update README.md
  • Loading branch information
pr8x authored Jun 23, 2023
1 parent 0d9f8f5 commit 3f06d27
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 26 deletions.
2 changes: 2 additions & 0 deletions DesktopNotifications.Apple/AppleNotificationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public void Dispose()
{
}

public NotificationManagerCapabilities Capabilities => NotificationManagerCapabilities.None;

public event EventHandler<NotificationActivatedEventArgs>? NotificationActivated;
public event EventHandler<NotificationDismissedEventArgs>? NotificationDismissed;

Expand Down
55 changes: 50 additions & 5 deletions DesktopNotifications.FreeDesktop/FreeDesktopNotificationManager.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tmds.DBus;

namespace DesktopNotifications.FreeDesktop
{
public class FreeDesktopNotificationManager : INotificationManager, IDisposable
public class FreeDesktopNotificationManager : INotificationManager
{
private const string NotificationsService = "org.freedesktop.Notifications";

private static readonly ObjectPath NotificationsPath = new ObjectPath("/org/freedesktop/Notifications");

private static Dictionary<string, NotificationManagerCapabilities> CapabilitiesMapping =
new Dictionary<string, NotificationManagerCapabilities>
{
{ "body", NotificationManagerCapabilities.BodyText },
{ "body-images", NotificationManagerCapabilities.BodyImages },
{ "body-markup", NotificationManagerCapabilities.BodyMarkup },
{ "sound", NotificationManagerCapabilities.Audio },
{ "icon", NotificationManagerCapabilities.Icon }
};

private readonly Dictionary<uint, Notification> _activeNotifications;
private readonly FreeDesktopApplicationContext _appContext;
private Connection? _connection;
Expand All @@ -34,7 +46,11 @@ public void Dispose()
_notificationCloseSubscription?.Dispose();
}

public NotificationManagerCapabilities Capabilities { get; private set; } =
NotificationManagerCapabilities.None;

public event EventHandler<NotificationActivatedEventArgs>? NotificationActivated;

public event EventHandler<NotificationDismissedEventArgs>? NotificationDismissed;

public string? LaunchActionId { get; }
Expand All @@ -54,10 +70,19 @@ public async Task Initialize()
OnNotificationActionInvoked,
OnNotificationActionInvokedError
);

_notificationCloseSubscription = await _proxy.WatchNotificationClosedAsync(
OnNotificationClosed,
OnNotificationClosedError
);

foreach (var cap in await _proxy.GetCapabilitiesAsync())
{
if (CapabilitiesMapping.TryGetValue(cap, out var capE))
{
Capabilities |= capE;
}
}
}

public async Task ShowNotification(Notification notification, DateTimeOffset? expirationTime = null)
Expand All @@ -77,7 +102,7 @@ public async Task ShowNotification(Notification notification, DateTimeOffset? ex
0,
_appContext.AppIcon ?? string.Empty,
notification.Title ?? throw new ArgumentException(),
notification.Body ?? throw new ArgumentException(),
GenerateNotificationBody(notification),
actions.ToArray(),
new Dictionary<string, object> { { "urgency", 1 } },
duration?.Milliseconds ?? 0
Expand Down Expand Up @@ -115,6 +140,26 @@ public async Task ScheduleNotification(
await ShowNotification(notification, expirationTime);
}

private string GenerateNotificationBody(Notification notification)
{
if (notification.Body == null)
{
throw new ArgumentException();
}

var sb = new StringBuilder();

sb.AppendLine(notification.Body);

if (Capabilities.HasFlag(NotificationManagerCapabilities.BodyImages) &&
notification.BodyImagePath is { } img)
{
sb.AppendLine($@"<img src=""{img}"" alt=""{notification.BodyImageAltText}""/>");
}

return sb.ToString();
}

private void CheckConnection()
{
if (_connection == null || _proxy == null)
Expand All @@ -132,7 +177,7 @@ private static IEnumerable<string> GenerateActions(Notification notification)
}
}

private void OnNotificationClosedError(Exception obj)
private static void OnNotificationClosedError(Exception obj)
{
throw obj;
}
Expand All @@ -144,7 +189,7 @@ private static NotificationDismissReason GetReason(uint reason)
1 => NotificationDismissReason.Expired,
2 => NotificationDismissReason.User,
3 => NotificationDismissReason.Application,
_ => throw new ArgumentOutOfRangeException()
_ => NotificationDismissReason.Unknown
};
}

Expand All @@ -167,7 +212,7 @@ private void OnNotificationClosed((uint id, uint reason) @event)
new NotificationDismissedEventArgs(notification, dismissReason));
}

private void OnNotificationActionInvokedError(Exception obj)
private static void OnNotificationActionInvokedError(Exception obj)
{
throw obj;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public void Dispose()

public string? LaunchActionId { get; }

public NotificationManagerCapabilities Capabilities => NotificationManagerCapabilities.None;

public event EventHandler<NotificationActivatedEventArgs>? NotificationActivated;

public event EventHandler<NotificationDismissedEventArgs>? NotificationDismissed;
Expand Down
18 changes: 18 additions & 0 deletions DesktopNotifications.Windows/WindowsNotificationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public WindowsNotificationManager(WindowsApplicationContext? applicationContext
_scheduledNotification = new Dictionary<ScheduledToastNotification, Notification>();
}

public NotificationManagerCapabilities Capabilities => NotificationManagerCapabilities.BodyText |
NotificationManagerCapabilities.BodyImages |
NotificationManagerCapabilities.Icon |
NotificationManagerCapabilities.Audio;

public event EventHandler<NotificationActivatedEventArgs>? NotificationActivated;

public event EventHandler<NotificationDismissedEventArgs>? NotificationDismissed;
Expand Down Expand Up @@ -161,6 +166,14 @@ private static XmlDocument GenerateXml(Notification notification)
xw.WriteString(notification.Body ?? string.Empty);
xw.WriteEndElement();

if (notification.BodyImagePath is { } img)
{
xw.WriteStartElement("image");
xw.WriteAttributeString("src", $"file:///{img}");
xw.WriteAttributeString("alt", notification.BodyImageAltText);
xw.WriteEndElement();
}

xw.WriteEndElement();

xw.WriteEndElement();
Expand Down Expand Up @@ -193,6 +206,11 @@ private static XmlDocument GenerateXml(Notification notification)
builder.AddText(notification.Title);
builder.AddText(notification.Body);

if (notification.BodyImagePath is { } img)
{
builder.AddInlineImage(new Uri($"file:///{img}"), notification.BodyImageAltText);
}

foreach (var (title, actionId) in notification.Buttons)
{
builder.AddButton(title, ToastActivationType.Foreground, actionId);
Expand Down
5 changes: 5 additions & 0 deletions DesktopNotifications/INotificationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public interface INotificationManager : IDisposable
/// </summary>
string? LaunchActionId { get; }

/// <summary>
/// Retrieve the capabilities of the notification manager (and its respective platform backend)
/// </summary>
NotificationManagerCapabilities Capabilities { get; }

/// <summary>
/// Raised when a notification was activated. The notion of "activation" varies from platform to platform.
/// </summary>
Expand Down
7 changes: 6 additions & 1 deletion DesktopNotifications/Notification.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;

namespace DesktopNotifications
{
Expand All @@ -15,6 +16,10 @@ public Notification()

public string? Body { get; set; }

public string? BodyImagePath { get; set; }

public string BodyImageAltText { get; set; } = "Image";

public List<(string Title, string ActionId)> Buttons { get; }
}
}
7 changes: 6 additions & 1 deletion DesktopNotifications/NotificationDismissReason.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public enum NotificationDismissReason
/// <summary>
/// The notification was explicitly removed by application code.
/// </summary>
Application
Application,

/// <summary>
///
/// </summary>
Unknown
}
}
15 changes: 15 additions & 0 deletions DesktopNotifications/NotificationManagerCapabilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace DesktopNotifications
{
[Flags]
public enum NotificationManagerCapabilities
{
None = 0,
BodyText = 1 << 0,
BodyImages = 1 << 1,
BodyMarkup = 1 << 2,
Audio = 1 << 3,
Icon = 1 << 4
}
}
9 changes: 5 additions & 4 deletions Example.Avalonia/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="400"
Width="400" Height="400"
Width="400" Height="500"
x:Class="Example.Avalonia.MainWindow"
Title="Example.Avalonia">

<StackPanel Margin="20" Spacing="10">
<TextBox Name="TitleTextBox" Watermark="Title" UseFloatingWatermark="True" />
<TextBox Name="BodyTextBox" Watermark="Body" UseFloatingWatermark="True" />
<TextBox Name="ImagePathTextBox" Watermark="Image path (optional)" UseFloatingWatermark="True" />

<Button Click="Show_OnClick" Content="Show Notification" />
<Button Click="Show_OnClick" Content="Show Notification" />
<Button Click="Schedule_OnClick" Content="Schedule notification (in 5 Seconds)" />
<Button Click="HideLast_OnClick" Content="Hide last notification" />

<TextBlock Foreground="Gray">Events:</TextBlock>
<ListBox Name="EventsListBox" Height="200" />
<TextBlock Foreground="Gray">Log:</TextBlock>
<ListBox Name="LogListBox" Height="200" />
</StackPanel>

</Window>
44 changes: 33 additions & 11 deletions Example.Avalonia/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
Expand All @@ -13,7 +14,8 @@ namespace Example.Avalonia
public class MainWindow : Window
{
private readonly TextBox _bodyTextBox;
private readonly ListBox _eventsListBox;
private readonly ListBox _logListBox;
private readonly TextBox _imagePathTextBox;
private readonly INotificationManager _notificationManager;
private readonly TextBox _titleTextBox;

Expand All @@ -28,33 +30,52 @@ public MainWindow()

_titleTextBox = this.FindControl<TextBox>("TitleTextBox");
_bodyTextBox = this.FindControl<TextBox>("BodyTextBox");
_eventsListBox = this.FindControl<ListBox>("EventsListBox");
_eventsListBox.Items = new ObservableCollection<string>();
_imagePathTextBox = this.FindControl<TextBox>("ImagePathTextBox");
_logListBox = this.FindControl<ListBox>("LogListBox");
_logListBox.Items = new ObservableCollection<string>();

_notificationManager = AvaloniaLocator.Current.GetService<INotificationManager>() ??
throw new InvalidOperationException("Missing notification manager");
_notificationManager.NotificationActivated += OnNotificationActivated;
_notificationManager.NotificationDismissed += OnNotificationDismissed;

Log($"Capabilities: {FormatFlagEnum(_notificationManager.Capabilities)}");

_bodyTextBox.IsEnabled =
_notificationManager.Capabilities.HasFlag(NotificationManagerCapabilities.BodyText);

_imagePathTextBox.IsEnabled =
_notificationManager.Capabilities.HasFlag(NotificationManagerCapabilities.BodyImages);

if (_notificationManager.LaunchActionId != null)
{
RegisterEvent($"Launch action: {_notificationManager.LaunchActionId}");
Log($"Launch action: {_notificationManager.LaunchActionId}");
}
}

private void RegisterEvent(string @event)
private static string FormatFlagEnum<TEnum>(TEnum e) where TEnum : Enum
{
var enumValues = (TEnum[])Enum.GetValues(typeof(TEnum));

return string.Join(", ",
enumValues
.Where(x => Convert.ToInt32(x) != 0 && e.HasFlag(x))
.Select(x => x.ToString()));
}

private void Log(string @event)
{
((IList<string>)_eventsListBox.Items).Add(@event);
((IList<string>)_logListBox.Items).Add(@event);
}

private void OnNotificationDismissed(object? sender, NotificationDismissedEventArgs e)
{
RegisterEvent($"Notification dismissed: {e.Reason}");
Log($"Notification dismissed: {e.Reason}");
}

private void OnNotificationActivated(object? sender, NotificationActivatedEventArgs e)
{
RegisterEvent($"Notification activated: {e.ActionId}");
Log($"Notification activated: {e.ActionId}");
}

private void InitializeComponent()
Expand All @@ -72,6 +93,7 @@ public async void Show_OnClick(object? sender, RoutedEventArgs e)
{
Title = _titleTextBox.Text ?? _titleTextBox.Watermark,
Body = _bodyTextBox.Text ?? _bodyTextBox.Watermark,
BodyImagePath = _imagePathTextBox.Text,
Buttons =
{
("This is awesome!", "awesome")
Expand All @@ -84,7 +106,7 @@ public async void Show_OnClick(object? sender, RoutedEventArgs e)
}
catch (Exception ex)
{
RegisterEvent(ex.Message);
Log(ex.Message);
}
}

Expand All @@ -106,7 +128,7 @@ await _notificationManager.ScheduleNotification(
}
catch (Exception ex)
{
RegisterEvent(ex.Message);
Log(ex.Message);
}
}

Expand All @@ -121,7 +143,7 @@ private async void HideLast_OnClick(object? sender, RoutedEventArgs e)
}
catch (Exception ex)
{
RegisterEvent(ex.Message);
Log(ex.Message);
}
}
}
Expand Down
Loading

0 comments on commit 3f06d27

Please sign in to comment.