diff --git a/src/TeamsISO.App/Services/UIPreferences.cs b/src/TeamsISO.App/Services/UIPreferences.cs
index b68e7a9..9434e8c 100644
--- a/src/TeamsISO.App/Services/UIPreferences.cs
+++ b/src/TeamsISO.App/Services/UIPreferences.cs
@@ -30,8 +30,10 @@ public static class UIPreferences
/// Sort modes for the participants DataGrid. is the default
/// and matches the engine's discovery order (operators with custom Stream Deck
/// layouts sometimes prefer Alphabetical for stability across meetings).
+ /// resorts at the 1Hz stats tick so the active
+ /// speaker bubbles to the top — useful for operators reacting to who's talking.
///
- public enum SortMode { JoinOrder, Alphabetical, OnlineFirst }
+ public enum SortMode { JoinOrder, Alphabetical, OnlineFirst, LoudestFirst }
/// The on-disk shape. New fields added here become opt-in for older files via default values.
public sealed record Prefs(
diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs
index 37c79e9..8c70116 100644
--- a/src/TeamsISO.App/ViewModels/MainViewModel.cs
+++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs
@@ -65,6 +65,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
///
public void SetSortMode(Services.UIPreferences.SortMode mode)
{
+ _currentSortMode = mode;
ParticipantsView.SortDescriptions.Clear();
switch (mode)
{
@@ -78,9 +79,20 @@ public sealed class MainViewModel : ObservableObject, IDisposable
ParticipantsView.SortDescriptions.Add(new SortDescription(
nameof(ParticipantViewModel.DisplayName), ListSortDirection.Ascending));
break;
+ case Services.UIPreferences.SortMode.LoudestFirst:
+ // Sort by the displayed audio level (which already includes the
+ // decay envelope) so participants don't snap-reorder on every
+ // tiny audio frame. ParticipantsView.Refresh() at the stats
+ // tick re-evaluates the sort with the latest values.
+ ParticipantsView.SortDescriptions.Add(new SortDescription(
+ nameof(ParticipantViewModel.DisplayedAudioLevel), ListSortDirection.Descending));
+ ParticipantsView.SortDescriptions.Add(new SortDescription(
+ nameof(ParticipantViewModel.DisplayName), ListSortDirection.Ascending));
+ break;
// JoinOrder: leave SortDescriptions empty.
}
}
+ private Services.UIPreferences.SortMode _currentSortMode = Services.UIPreferences.SortMode.JoinOrder;
///
/// Live filter substring. Empty = show everyone. Matched case-insensitively
/// against display name. Setter refreshes the view immediately so the
@@ -642,6 +654,16 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
}
+ // 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 —
+ // no need to pay the Refresh cost.
+ if (_currentSortMode == Services.UIPreferences.SortMode.LoudestFirst)
+ {
+ try { ParticipantsView.Refresh(); }
+ catch { /* defensive — Refresh occasionally throws on collection mutations */ }
+ }
+
// Update footer badges. Recording count is "ISOs that have a recorder
// attached" — _controller.RecordingEnabled tells us the global toggle,
// but the actual recorder count = number of running pipelines while