Quick-join Teams meetings from URL — paste link, click Join
Some checks failed
CI / build-and-test (push) Failing after 27s

Adds a small URL input + Join button to the IN-CALL bar. Operators paste a https://teams.microsoft.com/l/meetup-join/... or msteams:/l/meetup-join/... link, click Join, and Teams launches into the meeting in one shot. Eliminates the open-Teams → Calendar → find meeting → click join dance — operators get meeting links from email/Outlook and can now join straight from TeamsISO.

TeamsLauncher.TryJoinMeeting validates the URL targets Teams (only http(s) URLs containing teams.microsoft.com / teams.live.com, or msteams: deep-links — won't shell-exec arbitrary clipboard contents). On success, integrates with AutoHideTeamsWindows so the Teams meeting window briefly appears then vanishes; operator is in the call, driving routing from TeamsISO.

VM-side: MainViewModel.JoinMeetingCommand + JoinMeetingUrl two-way bound. Field clears on success; warn-toast on failure with the specific reason (empty / not-a-teams-url / launch-failed).
This commit is contained in:
Zac Gaetano 2026-05-10 20:45:04 -04:00
parent a9a10e01a4
commit b9147183ce
3 changed files with 122 additions and 0 deletions

View file

@ -624,6 +624,40 @@
VerticalAlignment="Center"/>
</StackPanel>
</Button>
<!-- Quick-join: paste a Teams meeting URL, click Join,
Teams launches into the meeting via the shell handler.
Avoids the open-Teams → Calendar → find meeting → click
join dance. Only enabled with non-empty text. -->
<Border Width="1"
Background="{DynamicResource Wd.Border}"
Margin="14,4,12,4"/>
<TextBox Text="{Binding JoinMeetingUrl, UpdateSourceTrigger=PropertyChanged}"
Width="240"
VerticalAlignment="Center"
Padding="10,5"
FontSize="11"
ToolTip="Paste a Teams meeting URL (https://teams.microsoft.com/l/meetup-join/... or msteams:/l/meetup-join/...) and click Join. TeamsISO hands it to Teams to launch + join in one step."/>
<Button Style="{StaticResource Wd.Button.Ghost}"
Command="{Binding JoinMeetingCommand}"
Padding="14,6"
Margin="6,0,0,0"
ToolTip="Hand the pasted meeting URL to Microsoft Teams. With auto-hide on (DISPLAY tab), the Teams window briefly appears then hides automatically.">
<StackPanel Orientation="Horizontal">
<Path Data="M 8,1 L 8,11 M 4,7 L 8,11 L 12,7 M 1,14 L 15,14"
Stroke="{DynamicResource Wd.Accent.Cyan}"
StrokeThickness="1.5"
Fill="Transparent"
Width="16" Height="15"
Stretch="None"
VerticalAlignment="Center"
Margin="0,0,8,0"/>
<TextBlock Text="Join"
Style="{StaticResource Wd.Text.Body}"
FontSize="12"
VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
</Border>

View file

@ -147,6 +147,58 @@ public static class TeamsLauncher
return asked;
}
/// <summary>
/// Hand a meeting URL off to the Teams shell handler. Accepts both the
/// <c>https://teams.microsoft.com/l/meetup-join/...</c> web format and
/// the <c>msteams:/l/meetup-join/...</c> 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.
/// </summary>
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;

View file

@ -163,6 +163,22 @@ public sealed class MainViewModel : ObservableObject, IDisposable
/// </summary>
public AsyncRelayCommand RollRecordingCommand { get; }
/// <summary>Join a Teams meeting from a pasted URL — see <see cref="JoinMeetingUrl"/>.</summary>
public RelayCommand JoinMeetingCommand { get; }
/// <summary>
/// Two-way bound to the quick-join input. Whatever the operator pastes
/// gets handed to <see cref="TeamsLauncher.TryJoinMeeting"/> when the
/// Join button fires. Cleared on success so the field is ready for the
/// next paste.
/// </summary>
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));