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;