using System.Windows.Threading; using TeamsISO.App.Services; namespace TeamsISO.App.ViewModels; // Teams launch / in-call / join-by-URL command helpers — split out of // MainViewModel.cs so the body methods don't live alongside the // constructor wiring + reactive subscriptions. The four command // PROPERTIES are declared back in MainViewModel.cs (public API surface); // this file holds the helpers they invoke. public sealed partial class MainViewModel { /// /// Wraps a invocation in a RelayCommand /// that translates the result to a user-visible toast. Centralizes the /// toast wording so the four control commands stay consistent. /// private RelayCommand MakeTeamsCommand(string label, Func invoke, string successMessage) { return new RelayCommand(() => { switch (invoke()) { case TeamsControlBridge.InvokeResult.Invoked: Toast.Show(successMessage); break; case TeamsControlBridge.InvokeResult.TeamsNotRunning: Toast.Warn("Teams isn't running."); break; case TeamsControlBridge.InvokeResult.ControlNotFound: Toast.Warn($"{label} control not found — are you in a call?"); break; case TeamsControlBridge.InvokeResult.InvokeFailed: Toast.Warn($"{label} button found but disabled."); break; } }); } /// /// Body of JoinMeetingCommand. Trims the pasted URL, hands it to /// , and runs the auto-hide /// follow-up if the operator has that preference set. /// private void JoinPastedMeeting() { 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 (Settings.AutoHideTeamsWindows) _ = TeamsLauncher.AutoHideAfterLaunchAsync(); } else { Toast.Warn($"Could not join: {error}"); } } /// /// Pull the meaningful "meeting title" out of Teams' raw window title. /// Teams uses formats like: /// "Weekly Standup | Microsoft Teams" /// "Meeting with Alice | Microsoft Teams" /// "Microsoft Teams" (no meeting, just the app) /// Strip the trailing " | Microsoft Teams" so the IN-CALL pill stays /// short and readable. Truncate beyond 50 chars so a long meeting /// subject doesn't push the rest of the IN-CALL bar off screen. /// internal static string ExtractMeetingTitle(string windowTitle) { if (string.IsNullOrWhiteSpace(windowTitle)) return string.Empty; var t = windowTitle.Trim(); foreach (var sep in new[] { " | Microsoft Teams", " | Teams", " - Microsoft Teams", " - Teams" }) { var idx = t.IndexOf(sep, StringComparison.Ordinal); if (idx > 0) { t = t.Substring(0, idx).Trim(); break; } } if (t.Equals("Microsoft Teams", StringComparison.OrdinalIgnoreCase)) return string.Empty; if (t.Length > 50) t = t.Substring(0, 47) + "…"; return t; } /// /// Meeting-state probe — runs on every 1Hz stats tick. We fire the UIA /// traversal on a worker thread because it can take 50–200ms in a busy /// call; the result is marshalled back to the dispatcher to update the /// view-model properties. One-tick latency on the displayed state is /// preferable to a UI hiccup. /// private void PollTeamsMeetingState() { try { var teamsRunning = TeamsLauncher.IsRunning(); if (!teamsRunning) { TeamsMeetingState = string.Empty; IsTeamsInCall = false; return; } _ = Task.Run(() => { try { // Single UIA traversal returns all three signals (in-call / // muted / camera-off) so we don't pay for three walks of // the same descendant tree at 1Hz. var snap = TeamsControlBridge.DetectCallState(); var inCall = snap.IsInCall; var title = inCall ? ExtractMeetingTitle(TeamsLauncher.GetActiveWindowTitle()) : null; _dispatcher.InvokeAsync(() => { IsTeamsInCall = inCall; TeamsMeetingState = inCall ? (string.IsNullOrEmpty(title) ? "IN CALL" : $"IN CALL · {title}") : "READY"; IsLocalMuted = inCall && (snap.IsMuted ?? false); IsLocalCameraOff = inCall && (snap.IsCameraOff ?? false); }); } catch { /* UIA flakiness shouldn't crash the stats tick */ } }); } catch { /* defensive — probe failures must never break the tick */ } } }