diff --git a/src/TeamsISO.App/Themes/WildDragonTheme.xaml b/src/TeamsISO.App/Themes/WildDragonTheme.xaml index 48f2f2a..7bb6914 100644 --- a/src/TeamsISO.App/Themes/WildDragonTheme.xaml +++ b/src/TeamsISO.App/Themes/WildDragonTheme.xaml @@ -740,6 +740,15 @@ + + + + + + diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs index c721abb..8183311 100644 --- a/src/TeamsISO.App/ViewModels/MainViewModel.cs +++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs @@ -752,6 +752,29 @@ public sealed class MainViewModel : ObservableObject, IDisposable } } + // Active-speaker highlight: find the loudest enabled participant + // and mark their IsActiveSpeaker flag. Only one row at a time; + // ties broken by enumeration order (first one wins). Threshold of + // 0.05 prevents constant flicker between near-silent participants + // when nobody's really speaking. + ParticipantViewModel? loudest = null; + double loudestLevel = 0.05; + foreach (var p in Participants) + { + if (!p.IsEnabled) continue; + if (p.DisplayedAudioLevel > loudestLevel) + { + loudest = p; + loudestLevel = p.DisplayedAudioLevel; + } + } + foreach (var p in Participants) + { + var shouldHighlight = ReferenceEquals(p, loudest); + if (p.IsActiveSpeaker != shouldHighlight) + p.IsActiveSpeaker = shouldHighlight; + } + // If sort mode is LoudestFirst, refresh the view so the new audio // peaks re-evaluate the sort. Skipped for the other sort modes // since their keys (name, online state) don't change every tick — diff --git a/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs b/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs index 21d0833..72d51c7 100644 --- a/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs +++ b/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs @@ -44,6 +44,19 @@ public sealed class ParticipantViewModel : ObservableObject /// True when is non-null. Bound to Visibility in XAML. public bool HasThumbnail => _thumbnail is not null; + /// + /// True when this participant is currently the loudest among the live + /// set — set by MainViewModel at the 1Hz stats tick. Bound to a cyan + /// border accent on the DataGrid row so operators can spot who's + /// speaking without watching every VU bar individually. + /// + public bool IsActiveSpeaker + { + get => _isActiveSpeaker; + internal set => SetField(ref _isActiveSpeaker, value); + } + private bool _isActiveSpeaker; + public ParticipantViewModel(IIsoController controller, Participant participant, ToastViewModel? toast = null) { _controller = controller;