diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index 1619973..6f7347d 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -624,6 +624,40 @@ VerticalAlignment="Center"/> + + + + + diff --git a/src/TeamsISO.App/Services/TeamsLauncher.cs b/src/TeamsISO.App/Services/TeamsLauncher.cs index 43cef4d..db1a72c 100644 --- a/src/TeamsISO.App/Services/TeamsLauncher.cs +++ b/src/TeamsISO.App/Services/TeamsLauncher.cs @@ -147,6 +147,58 @@ public static class TeamsLauncher return asked; } + /// + /// Hand a meeting URL off to the Teams shell handler. Accepts both the + /// https://teams.microsoft.com/l/meetup-join/... web format and + /// the msteams:/l/meetup-join/... deep-link form (either causes + /// Teams to launch + join the meeting in one shot — the OS shell maps + /// teams.microsoft.com URLs to the registered ms-teams: handler). + /// + /// Use case: operator pastes a meeting link they got over email / chat + /// into TeamsISO's quick-join field instead of opening Teams, + /// hunting down the calendar entry, and clicking Join. With auto-hide + /// on, the Teams window flashes briefly then disappears; the operator + /// is now in the meeting, driving routing from TeamsISO. + /// + /// Returns true if the shell accepted the URL; false if URL is malformed + /// or rejected. errorMessage populated on failure. + /// + public static bool TryJoinMeeting(string url, out string? errorMessage) + { + errorMessage = null; + if (string.IsNullOrWhiteSpace(url)) + { + errorMessage = "URL is empty."; + return false; + } + + var trimmed = url.Trim(); + + // Defensive sanity-check: only accept URLs that obviously target + // Teams. We don't want to invoke arbitrary shell handlers from a + // clipboard paste — if someone pastes "calc.exe" into the input we + // shouldn't launch it. Specifically: http(s) URLs must contain + // "teams.microsoft.com" or "teams.live.com"; otherwise must start + // with "msteams:". + var lower = trimmed.ToLowerInvariant(); + var looksLikeTeams = + lower.StartsWith("msteams:") || + (lower.StartsWith("http://") || lower.StartsWith("https://")) && + (lower.Contains("teams.microsoft.com") || lower.Contains("teams.live.com")); + if (!looksLikeTeams) + { + errorMessage = "Not a Microsoft Teams meeting URL. " + + "Expected a https://teams.microsoft.com/l/meetup-join/... " + + "or msteams:/l/meetup-join/... link."; + return false; + } + + if (TryStart(trimmed, useShell: true, out var err)) + return true; + errorMessage = err; + return false; + } + private static bool TryStart(string target, bool useShell, out string error, string? arguments = null) { error = string.Empty; diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs index f3473b5..7ca8f28 100644 --- a/src/TeamsISO.App/ViewModels/MainViewModel.cs +++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs @@ -163,6 +163,22 @@ public sealed class MainViewModel : ObservableObject, IDisposable /// public AsyncRelayCommand RollRecordingCommand { get; } + /// Join a Teams meeting from a pasted URL — see . + public RelayCommand JoinMeetingCommand { get; } + + /// + /// Two-way bound to the quick-join input. Whatever the operator pastes + /// gets handed to when the + /// Join button fires. Cleared on success so the field is ready for the + /// next paste. + /// + public string JoinMeetingUrl + { + get => _joinMeetingUrl; + set => SetField(ref _joinMeetingUrl, value); + } + private string _joinMeetingUrl = string.Empty; + public string StatusText { get => _statusText; @@ -328,6 +344,26 @@ public sealed class MainViewModel : ObservableObject, IDisposable notes.Show(); // non-modal so operators can stamp + read alongside the show }); + JoinMeetingCommand = new RelayCommand(() => + { + // Trim + handle the operator pasting whitespace around the URL. + var url = (_joinMeetingUrl ?? string.Empty).Trim(); + if (string.IsNullOrEmpty(url)) return; + if (TeamsLauncher.TryJoinMeeting(url, out var error)) + { + Toast.Show("Joining Teams meeting…"); + JoinMeetingUrl = string.Empty; + // If the operator has auto-hide on, kick off the hide watcher + // so the Teams meeting window goes away as soon as it renders. + if (Settings.AutoHideTeamsWindows) + _ = TeamsLauncher.AutoHideAfterLaunchAsync(); + } + else + { + Toast.Warn($"Could not join: {error}"); + } + }); + RollRecordingCommand = new AsyncRelayCommand(RollRecordingAsync, () => _controller.RecordingEnabled && Participants.Any(p => p.IsEnabled));