#if WINDOWS using System.Collections.Concurrent; using CommunityToolkit.WinUI.Notifications; using MeetingAssistant.Notifications; using MeetingAssistant.Recording; namespace MeetingAssistant.Calendar; public sealed class WindowsMeetingStartPromptService : IMeetingStartPromptService, IHostedService, IDisposable { private const string NotificationSource = "meeting-start-prompt"; private const string NotificationGroup = "meeting-start"; private readonly ConcurrentDictionary> pendingPrompts = new(StringComparer.OrdinalIgnoreCase); private readonly ILogger logger; private readonly object registrationGate = new(); private bool registered; public WindowsMeetingStartPromptService(ILogger logger) { this.logger = logger; } public Task ShowPromptAsync( MeetingStartPromptRequest request, Func handleResponseAsync, CancellationToken cancellationToken) { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763)) { logger.LogWarning("Windows app notifications require Windows 10 version 1809 or later"); return Task.CompletedTask; } if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } try { EnsureRegistered(); var promptId = Guid.NewGuid().ToString("N"); pendingPrompts[promptId] = handleResponseAsync; BuildNotification(promptId, request).Show(toast => { toast.Group = NotificationGroup; toast.Tag = promptId; toast.ExpirationTime = MeetingToastExpirationPolicy.StartRecordingPromptExpiration(DateTimeOffset.Now); }); logger.LogInformation( "Displayed native Windows meeting-start notification {PromptId} for {Subject}", promptId, request.Meeting.Subject); } catch (Exception exception) { logger.LogError(exception, "Failed to display native Windows meeting-start notification"); } return Task.CompletedTask; } public Task StartAsync(CancellationToken cancellationToken) { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763)) { logger.LogWarning("Windows app notifications require Windows 10 version 1809 or later"); return Task.CompletedTask; } EnsureRegistered(); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public void Dispose() { if (!registered) { return; } try { ToastNotificationManagerCompat.OnActivated -= OnNotificationInvoked; } catch (Exception exception) { logger.LogWarning(exception, "Failed to detach native Windows meeting-start notification activation handler"); } } private void EnsureRegistered() { if (registered) { return; } lock (registrationGate) { if (registered) { return; } ToastNotificationManagerCompat.OnActivated += OnNotificationInvoked; registered = true; logger.LogInformation("Registered native Windows meeting-start notification activation handler"); } } private static ToastContentBuilder BuildNotification( string promptId, MeetingStartPromptRequest request) { var yesButton = new ToastButton() .SetContent("Yes") .AddArgument("source", NotificationSource) .AddArgument("promptId", promptId) .AddArgument("response", "record") .SetBackgroundActivation(); var noButton = new ToastButton() .SetContent("No") .AddArgument("source", NotificationSource) .AddArgument("promptId", promptId) .AddArgument("response", "skip") .SetBackgroundActivation(); return new ToastContentBuilder() .AddArgument("source", NotificationSource) .AddArgument("promptId", promptId) .SetToastScenario(ToastScenario.Reminder) .SetToastDuration(ToastDuration.Long) .AddText("Record meeting?") .AddText($"{request.Meeting.Subject} starts at {request.Meeting.Start.LocalDateTime:t}.") .AddButton(yesButton) .AddButton(noButton); } private void OnNotificationInvoked(ToastNotificationActivatedEventArgsCompat args) { var arguments = NotificationActivationArguments.Parse(args.Argument); if (!TryGetArgument(arguments, "source", out var source) || !string.Equals(source, NotificationSource, StringComparison.OrdinalIgnoreCase)) { return; } if (!TryGetArgument(arguments, "promptId", out var promptId)) { logger.LogWarning("Ignoring native Windows meeting-start notification activation without prompt id"); return; } if (!pendingPrompts.TryRemove(promptId, out var handleResponseAsync)) { logger.LogWarning( "Ignoring native Windows meeting-start notification {PromptId} activation because no pending callback exists", promptId); return; } var response = TryGetArgument(arguments, "response", out var responseValue) && string.Equals(responseValue, "record", StringComparison.OrdinalIgnoreCase) ? MeetingStartPromptResponse.Record : MeetingStartPromptResponse.Skip; _ = Task.Run(async () => { try { logger.LogInformation( "Native Windows meeting-start notification {PromptId} activated with response {Response}", promptId, response); await handleResponseAsync(response, CancellationToken.None); } catch (Exception exception) { logger.LogError( exception, "Failed to handle native Windows meeting-start notification response {Response}", response); } }); } private static bool TryGetArgument( IReadOnlyDictionary arguments, string key, out string value) { if (arguments.TryGetValue(key, out value!)) { return true; } value = ""; return false; } } #endif