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