mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
perf: fix O(n) allocations, freeze brushes, improve disposal and nullability
- Replace 5x ToList().FindIndex() with direct for-loops in MainViewModel - UpdateUnreadCount now updates ChannelGroup in-place instead of full rebuild - Remove redundant RebuildChannelGroups() call in OnReady - Freeze all SolidColorBrush instances in converters for thread safety - EmojiPicker search shows empty state instead of fallback to all categories - MainViewModel implements IDisposable for _typingTimer cleanup - ApiMessage.Username changed to string? to match server reality
This commit is contained in:
@@ -59,8 +59,6 @@ public partial class EmojiPickerControl : UserControl
|
||||
.Where(c => c.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
CategoryList.ItemsSource = filtered.Count > 0
|
||||
? filtered
|
||||
: EmojiData.Categories;
|
||||
CategoryList.ItemsSource = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ public sealed class HexColorToBrushConverter : IValueConverter
|
||||
try
|
||||
{
|
||||
var color = (Color)ColorConverter.ConvertFromString(hex);
|
||||
return new SolidColorBrush(color);
|
||||
var brush = new SolidColorBrush(color);
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -24,7 +26,9 @@ public sealed class HexColorToBrushConverter : IValueConverter
|
||||
}
|
||||
|
||||
// Default fallback color (muted text)
|
||||
return new SolidColorBrush((Color)ColorConverter.ConvertFromString("#949ba4"));
|
||||
var fallback = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#949ba4"));
|
||||
fallback.Freeze();
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
@@ -35,10 +39,17 @@ public sealed class HexColorToBrushConverter : IValueConverter
|
||||
[ValueConversion(typeof(string), typeof(SolidColorBrush))]
|
||||
public sealed class StatusToBrushConverter : IValueConverter
|
||||
{
|
||||
private static readonly SolidColorBrush Online = new((Color)ColorConverter.ConvertFromString("#23a55a"));
|
||||
private static readonly SolidColorBrush Idle = new((Color)ColorConverter.ConvertFromString("#f0b232"));
|
||||
private static readonly SolidColorBrush Dnd = new((Color)ColorConverter.ConvertFromString("#f23f43"));
|
||||
private static readonly SolidColorBrush Offline = new((Color)ColorConverter.ConvertFromString("#6d6f78"));
|
||||
private static readonly SolidColorBrush Online = CreateFrozen("#23a55a");
|
||||
private static readonly SolidColorBrush Idle = CreateFrozen("#f0b232");
|
||||
private static readonly SolidColorBrush Dnd = CreateFrozen("#f23f43");
|
||||
private static readonly SolidColorBrush Offline = CreateFrozen("#6d6f78");
|
||||
|
||||
private static SolidColorBrush CreateFrozen(string hex)
|
||||
{
|
||||
var brush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex));
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
@@ -70,8 +81,15 @@ public sealed class FirstLetterConverter : IValueConverter
|
||||
[ValueConversion(typeof(bool), typeof(SolidColorBrush))]
|
||||
public sealed class BoolToRedBrushConverter : IValueConverter
|
||||
{
|
||||
private static readonly SolidColorBrush Red = new((Color)ColorConverter.ConvertFromString("#f23f43"));
|
||||
private static readonly SolidColorBrush Normal = new((Color)ColorConverter.ConvertFromString("#b5bac1"));
|
||||
private static readonly SolidColorBrush Red = CreateFrozen("#f23f43");
|
||||
private static readonly SolidColorBrush Normal = CreateFrozen("#b5bac1");
|
||||
|
||||
private static SolidColorBrush CreateFrozen(string hex)
|
||||
{
|
||||
var brush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex));
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is true ? Red : Normal;
|
||||
@@ -84,8 +102,15 @@ public sealed class BoolToRedBrushConverter : IValueConverter
|
||||
[ValueConversion(typeof(bool), typeof(SolidColorBrush))]
|
||||
public sealed class SpeakingToStrokeBrushConverter : IValueConverter
|
||||
{
|
||||
private static readonly SolidColorBrush Speaking = new((Color)ColorConverter.ConvertFromString("#23a55a"));
|
||||
private static readonly SolidColorBrush Silent = new(Colors.Transparent);
|
||||
private static readonly SolidColorBrush Speaking = CreateFrozen((Color)ColorConverter.ConvertFromString("#23a55a"));
|
||||
private static readonly SolidColorBrush Silent = CreateFrozen(Colors.Transparent);
|
||||
|
||||
private static SolidColorBrush CreateFrozen(Color color)
|
||||
{
|
||||
var brush = new SolidColorBrush(color);
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is true ? Speaking : Silent;
|
||||
|
||||
@@ -50,7 +50,7 @@ public record ApiMessage(
|
||||
[property: JsonPropertyName("deleted")] bool Deleted,
|
||||
[property: JsonPropertyName("pinned")] bool Pinned,
|
||||
[property: JsonPropertyName("timestamp")] string Timestamp,
|
||||
[property: JsonPropertyName("username")] string Username,
|
||||
[property: JsonPropertyName("username")] string? Username,
|
||||
[property: JsonPropertyName("avatar")] string? Avatar,
|
||||
[property: JsonPropertyName("attachments")] IReadOnlyList<ApiAttachment>? Attachments = null
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ using OwnCord.Client.Services;
|
||||
|
||||
namespace OwnCord.Client.ViewModels;
|
||||
|
||||
public sealed class MainViewModel : ViewModelBase
|
||||
public sealed class MainViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
private IChatService? _chat;
|
||||
private Channel? _selectedChannel;
|
||||
@@ -444,11 +444,28 @@ public sealed class MainViewModel : ViewModelBase
|
||||
|
||||
public void UpdateUnreadCount(long channelId, int count)
|
||||
{
|
||||
var idx = Channels.ToList().FindIndex(c => c.Id == channelId);
|
||||
if (idx < 0) return;
|
||||
var updated = Channels[idx] with { UnreadCount = count };
|
||||
Channels[idx] = updated;
|
||||
RebuildChannelGroups();
|
||||
for (var i = 0; i < Channels.Count; i++)
|
||||
{
|
||||
if (Channels[i].Id == channelId)
|
||||
{
|
||||
if (Channels[i].UnreadCount == count) return;
|
||||
Channels[i] = Channels[i] with { UnreadCount = count };
|
||||
|
||||
// Update the specific ChannelItem in-place instead of rebuilding all groups
|
||||
foreach (var group in ChannelGroups)
|
||||
{
|
||||
for (var j = 0; j < group.Items.Count; j++)
|
||||
{
|
||||
if (group.Items[j].Channel.Id == channelId)
|
||||
{
|
||||
group.Items[j] = new ChannelItem { Channel = Channels[i] };
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowTyping(string username)
|
||||
@@ -780,7 +797,6 @@ public sealed class MainViewModel : ViewModelBase
|
||||
};
|
||||
Channels.Add(new Channel(ch.Id, ch.Name, type, ch.Category, ch.Position, 0, null, ch.Topic));
|
||||
}
|
||||
RebuildChannelGroups();
|
||||
|
||||
// Load members
|
||||
Members.Clear();
|
||||
@@ -867,8 +883,6 @@ public sealed class MainViewModel : ViewModelBase
|
||||
|
||||
private void OnPresence(PresencePayload payload)
|
||||
{
|
||||
var idx = Members.ToList().FindIndex(m => m.Id == payload.UserId);
|
||||
if (idx < 0) return;
|
||||
var status = payload.Status switch
|
||||
{
|
||||
"online" => UserStatus.Online,
|
||||
@@ -876,24 +890,41 @@ public sealed class MainViewModel : ViewModelBase
|
||||
"dnd" => UserStatus.Dnd,
|
||||
_ => UserStatus.Offline
|
||||
};
|
||||
Members[idx] = Members[idx] with { Status = status };
|
||||
RebuildMemberGroups();
|
||||
for (var i = 0; i < Members.Count; i++)
|
||||
{
|
||||
if (Members[i].Id == payload.UserId)
|
||||
{
|
||||
Members[i] = Members[i] with { Status = status };
|
||||
RebuildMemberGroups();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChatEdited(ChatEditedPayload payload)
|
||||
{
|
||||
var idx = Messages.ToList().FindIndex(m => m.Id == payload.MessageId);
|
||||
if (idx < 0) return;
|
||||
Messages[idx] = Messages[idx] with { Content = payload.Content, EditedAt = payload.EditedAt };
|
||||
RebuildDisplayMessages();
|
||||
for (var i = 0; i < Messages.Count; i++)
|
||||
{
|
||||
if (Messages[i].Id == payload.MessageId)
|
||||
{
|
||||
Messages[i] = Messages[i] with { Content = payload.Content, EditedAt = payload.EditedAt };
|
||||
RebuildDisplayMessages();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChatDeleted(ChatDeletedPayload payload)
|
||||
{
|
||||
var idx = Messages.ToList().FindIndex(m => m.Id == payload.MessageId);
|
||||
if (idx < 0) return;
|
||||
Messages[idx] = Messages[idx] with { Deleted = true, Content = "[deleted]" };
|
||||
RebuildDisplayMessages();
|
||||
for (var i = 0; i < Messages.Count; i++)
|
||||
{
|
||||
if (Messages[i].Id == payload.MessageId)
|
||||
{
|
||||
Messages[i] = Messages[i] with { Deleted = true, Content = "[deleted]" };
|
||||
RebuildDisplayMessages();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMemberJoined(WsMember payload)
|
||||
@@ -927,7 +958,15 @@ public sealed class MainViewModel : ViewModelBase
|
||||
|
||||
private void OnChannelUpdated(ChannelEventPayload payload)
|
||||
{
|
||||
var idx = Channels.ToList().FindIndex(c => c.Id == payload.Id);
|
||||
var idx = -1;
|
||||
for (var i = 0; i < Channels.Count; i++)
|
||||
{
|
||||
if (Channels[i].Id == payload.Id)
|
||||
{
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) return;
|
||||
var type = payload.Type switch
|
||||
{
|
||||
@@ -1028,4 +1067,12 @@ public sealed class MainViewModel : ViewModelBase
|
||||
var ch = Channels.FirstOrDefault(c => c.Id == channelId);
|
||||
return ch?.UnreadCount ?? 0;
|
||||
}
|
||||
|
||||
// ── IDisposable ──────────────────────────────────────────────────────────
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_typingTimer?.Dispose();
|
||||
_typingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user