Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image support #21

Merged
merged 10 commits into from
Jun 23, 2023
Merged
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
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