Public Access
Add Outlook Teams recording prompts
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
#if WINDOWS
|
||||
using System.Collections.Concurrent;
|
||||
using CommunityToolkit.WinUI.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<string, Func<MeetingStartPromptResponse, CancellationToken, Task>> pendingPrompts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ILogger<WindowsMeetingStartPromptService> logger;
|
||||
private readonly object registrationGate = new();
|
||||
private bool registered;
|
||||
|
||||
public WindowsMeetingStartPromptService(ILogger<WindowsMeetingStartPromptService> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public Task ShowPromptAsync(
|
||||
MeetingStartPromptRequest request,
|
||||
Func<MeetingStartPromptResponse, CancellationToken, Task> 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 = request.Meeting.End;
|
||||
});
|
||||
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)
|
||||
.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<string, string> arguments,
|
||||
string key,
|
||||
out string value)
|
||||
{
|
||||
if (arguments.TryGetValue(key, out value!))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
value = "";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user