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));