From b08358baed0279851c27d8febc4b46de50ebcaf7 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:38:45 +0200 Subject: [PATCH] Code cleanup --- Decho/Models/ChannelModel.cs | 28 +- Decho/Models/MessageModel.cs | 48 +- Decho/Models/ServerModel.cs | 36 +- Decho/Models/UserModel.cs | 28 +- Decho/Program.cs | 13 +- Decho/Services/ConnectionService.cs | 761 +++++++++++-------- Decho/ViewLocator.cs | 10 +- Decho/ViewModels/ChannelViewModel.cs | 24 +- Decho/ViewModels/ChatViewModel.cs | 76 +- Decho/ViewModels/MainWindowViewModel.cs | 518 +++++++------ Decho/ViewModels/MessageComposerViewModel.cs | 59 +- Decho/ViewModels/MessageViewModel.cs | 24 +- Decho/ViewModels/ServerViewModel.cs | 212 +++--- Decho/ViewModels/SidebarViewModel.cs | 42 +- Decho/ViewModels/ViewModelBase.cs | 6 +- Decho/Views/ChannelListView.axaml.cs | 2 +- Decho/Views/ChatView.axaml.cs | 2 +- Decho/Views/MessageComposerView.axaml.cs | 31 +- Decho/Views/MessageItemView.axaml.cs | 81 +- Decho/Views/MessageListView.axaml.cs | 32 +- Decho/Views/SidebarView.axaml.cs | 12 +- 21 files changed, 1065 insertions(+), 980 deletions(-) diff --git a/Decho/Models/ChannelModel.cs b/Decho/Models/ChannelModel.cs index 22638e3..b964145 100644 --- a/Decho/Models/ChannelModel.cs +++ b/Decho/Models/ChannelModel.cs @@ -2,29 +2,15 @@ using System.Collections.ObjectModel; namespace Decho.Models; -public sealed class ChannelModel +public sealed class ChannelModel(string id, string name, ObservableCollection messages, string? topic = null, bool isPublic = true) { - public ChannelModel( - string id, - string name, - ObservableCollection messages, - string? topic = null, - bool isPublic = true) - { - Id = id; - Name = name; - Messages = messages; - Topic = topic; - IsPublic = isPublic; - } + public string Id { get; } = id; - public string Id { get; } + public string Name { get; } = name; - public string Name { get; } + public string? Topic { get; set; } = topic; - public string? Topic { get; set; } + public bool IsPublic { get; } = isPublic; - public bool IsPublic { get; } - - public ObservableCollection Messages { get; } -} + public ObservableCollection Messages { get; } = messages; +} \ No newline at end of file diff --git a/Decho/Models/MessageModel.cs b/Decho/Models/MessageModel.cs index 9d546fa..8a96d06 100644 --- a/Decho/Models/MessageModel.cs +++ b/Decho/Models/MessageModel.cs @@ -2,49 +2,25 @@ using EchoHub.Core.Models; namespace Decho.Models; -public sealed class MessageModel +public sealed class MessageModel(string id, UserModel author, DateTimeOffset sentAt, string content, string channelName, string? serverUrl = null, MessageType type = MessageType.Text, string? attachmentUrl = null, string? attachmentFileName = null, long? attachmentFileSize = null) { - public MessageModel( - string id, - UserModel author, - DateTimeOffset sentAt, - string content, - string channelName, - string? serverUrl = null, - MessageType type = MessageType.Text, - string? attachmentUrl = null, - string? attachmentFileName = null, - long? attachmentFileSize = null) - { - Id = id; - Author = author; - SentAt = sentAt; - Content = content; - ChannelName = channelName; - ServerUrl = serverUrl; - Type = type; - AttachmentUrl = attachmentUrl; - AttachmentFileName = attachmentFileName; - AttachmentFileSize = attachmentFileSize; - } + public string Id { get; } = id; - public string Id { get; } + public UserModel Author { get; } = author; - public UserModel Author { get; } + public DateTimeOffset SentAt { get; } = sentAt; - public DateTimeOffset SentAt { get; } + public string Content { get; } = content; - public string Content { get; } + public string ChannelName { get; } = channelName; - public string ChannelName { get; } + public string? ServerUrl { get; } = serverUrl; - public string? ServerUrl { get; } + public MessageType Type { get; } = type; - public MessageType Type { get; } + public string? AttachmentUrl { get; } = attachmentUrl; - public string? AttachmentUrl { get; } + public string? AttachmentFileName { get; } = attachmentFileName; - public string? AttachmentFileName { get; } - - public long? AttachmentFileSize { get; } -} + public long? AttachmentFileSize { get; } = attachmentFileSize; +} \ No newline at end of file diff --git a/Decho/Models/ServerModel.cs b/Decho/Models/ServerModel.cs index 3e5e4eb..8a38e3c 100644 --- a/Decho/Models/ServerModel.cs +++ b/Decho/Models/ServerModel.cs @@ -2,37 +2,19 @@ using System.Collections.ObjectModel; namespace Decho.Models; -public sealed class ServerModel +public sealed class ServerModel(string id, string name, ObservableCollection channels, string serverUrl = "", bool isConnected = false, bool isConnecting = false, string? connectedUser = null) { - public ServerModel( - string id, - string name, - ObservableCollection channels, - string serverUrl = "", - bool isConnected = false, - bool isConnecting = false, - string? connectedUser = null) - { - Id = id; - Name = name; - Channels = channels; - ServerUrl = serverUrl; - IsConnected = isConnected; - IsConnecting = isConnecting; - ConnectedUser = connectedUser; - } + public string Id { get; } = id; - public string Id { get; } + public string Name { get; } = name; - public string Name { get; } + public string ServerUrl { get; set; } = serverUrl; - public string ServerUrl { get; set; } + public bool IsConnected { get; set; } = isConnected; - public bool IsConnected { get; set; } + public bool IsConnecting { get; set; } = isConnecting; - public bool IsConnecting { get; set; } + public string? ConnectedUser { get; set; } = connectedUser; - public string? ConnectedUser { get; set; } - - public ObservableCollection Channels { get; } -} + public ObservableCollection Channels { get; } = channels; +} \ No newline at end of file diff --git a/Decho/Models/UserModel.cs b/Decho/Models/UserModel.cs index 32a4387..063b5a8 100644 --- a/Decho/Models/UserModel.cs +++ b/Decho/Models/UserModel.cs @@ -2,29 +2,15 @@ using EchoHub.Core.Models; namespace Decho.Models; -public sealed class UserModel +public sealed class UserModel(string id, string displayName, string? nicknameColor = null, UserStatus status = UserStatus.Online, string? statusMessage = null) { - public UserModel( - string id, - string displayName, - string? nicknameColor = null, - UserStatus status = UserStatus.Online, - string? statusMessage = null) - { - Id = id; - DisplayName = displayName; - NicknameColor = nicknameColor; - Status = status; - StatusMessage = statusMessage; - } + public string Id { get; } = id; - public string Id { get; } + public string DisplayName { get; } = displayName; - public string DisplayName { get; } + public string? NicknameColor { get; set; } = nicknameColor; - public string? NicknameColor { get; set; } + public UserStatus Status { get; set; } = status; - public UserStatus Status { get; set; } - - public string? StatusMessage { get; set; } -} + public string? StatusMessage { get; set; } = statusMessage; +} \ No newline at end of file diff --git a/Decho/Program.cs b/Decho/Program.cs index 9b6a31a..c37192d 100644 --- a/Decho/Program.cs +++ b/Decho/Program.cs @@ -6,23 +6,24 @@ using Avalonia; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; -using System; - namespace Decho; -internal sealed class Program +internal static class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() + public static void Main(string[] args) + { + _ = BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() { - IconProvider.Current + _ = IconProvider.Current .Register(); return AppBuilder.Configure() @@ -31,4 +32,4 @@ internal sealed class Program .LogToTrace() .UseReactiveUI(); } -} +} \ No newline at end of file diff --git a/Decho/Services/ConnectionService.cs b/Decho/Services/ConnectionService.cs index 5434381..542f7f2 100644 --- a/Decho/Services/ConnectionService.cs +++ b/Decho/Services/ConnectionService.cs @@ -1,38 +1,45 @@ -using System.Collections.ObjectModel; +using Decho.Models; + +using EchoHub.Client.Commands; using EchoHub.Client.Config; using EchoHub.Client.Services; -using EchoHub.Client.Commands; +using EchoHub.Client.UI.Dialogs; using EchoHub.Core.Constants; using EchoHub.Core.DTOs; using EchoHub.Core.Models; -using Decho.Models; + +using System.Collections.ObjectModel; namespace Decho.Services; public sealed class ConnectionService : IDisposable { - private readonly Dictionary _connections = new(StringComparer.OrdinalIgnoreCase); - public event Action? ServerAdded; + public event Action? ServerRemoved; + public event Action? ServerStateChanged; + public event Action? ChannelAdded; + public event Action? ChannelRemoved; + public event Action? MessageReceived; + public event Action? UserJoined; + public event Action? UserLeft; + public event Action? ErrorOccurred; + private readonly Dictionary _connections = new(StringComparer.OrdinalIgnoreCase); internal IReadOnlyDictionary Connections => _connections; - internal ServerConnection? GetConnection(string serverUrl) => - _connections.TryGetValue(serverUrl, out var conn) ? conn : null; - public async Task ConnectAsync(string serverUrl, string username, string password, bool isRegister, bool rememberMe) { - var conn = new ConnectionManager(); + ConnectionManager conn = new ConnectionManager(); - var dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult( + ConnectDialogResult dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult( serverUrl, username, password, isRegister, rememberMe, null); ConnectResult result; @@ -46,12 +53,12 @@ public sealed class ConnectionService : IDisposable throw; } - var login = result.Login; - var userModel = new UserModel(login.Username, login.DisplayName ?? login.Username, + LoginResponse login = result.Login; + UserModel userModel = new UserModel(login.Username, login.DisplayName ?? login.Username, login.NicknameColor); - var channels = new ObservableCollection(); - var serverModel = new ServerModel( + ObservableCollection channels = []; + ServerModel serverModel = new ServerModel( Guid.NewGuid().ToString("N"), new Uri(serverUrl).Host, channels, @@ -59,11 +66,11 @@ public sealed class ConnectionService : IDisposable isConnected: true, connectedUser: login.Username); - var serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel); + ServerConnection serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel); - foreach (var ch in result.Channels) + foreach (ChannelDto ch in result.Channels) { - var channelModel = ChannelModelFromDto(ch); + ChannelModel channelModel = ChannelModelFromDto(ch); channels.Add(channelModel); } @@ -78,9 +85,9 @@ public sealed class ConnectionService : IDisposable public async Task ConnectWithSavedTokenAsync(string serverUrl, string username, string refreshToken, bool rememberMe) { - var conn = new ConnectionManager(); + ConnectionManager conn = new ConnectionManager(); - var dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult( + ConnectDialogResult dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult( serverUrl, username, "", false, rememberMe, refreshToken); ConnectResult result; @@ -94,11 +101,11 @@ public sealed class ConnectionService : IDisposable throw; } - var login = result.Login; - var userModel = new UserModel(login.Username, login.DisplayName ?? login.Username, login.NicknameColor); + LoginResponse login = result.Login; + UserModel userModel = new UserModel(login.Username, login.DisplayName ?? login.Username, login.NicknameColor); - var channels = new ObservableCollection(); - var serverModel = new ServerModel( + ObservableCollection channels = []; + ServerModel serverModel = new ServerModel( Guid.NewGuid().ToString("N"), new Uri(serverUrl).Host, channels, @@ -106,11 +113,11 @@ public sealed class ConnectionService : IDisposable isConnected: true, connectedUser: login.Username); - var serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel); + ServerConnection serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel); - foreach (var ch in result.Channels) + foreach (ChannelDto ch in result.Channels) { - var channelModel = ChannelModelFromDto(ch); + ChannelModel channelModel = ChannelModelFromDto(ch); channels.Add(channelModel); } @@ -121,15 +128,395 @@ public sealed class ConnectionService : IDisposable ServerAdded?.Invoke(serverModel); } + public async Task DisconnectAsync(string serverUrl) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + entry.Server.IsConnected = false; + entry.Server.IsConnecting = false; + ServerStateChanged?.Invoke(entry.Server); + + await entry.Manager.CleanupAsync(); + entry.ApiClient.Dispose(); + await entry.Manager.DisposeAsync(); + + _ = _connections.Remove(serverUrl); + ServerRemoved?.Invoke(serverUrl); + } + + public async Task SendMessageAsync(string serverUrl, string channelName, string content) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + throw new InvalidOperationException("Not connected to server"); + } + + await entry.Manager.SendMessageAsync(channelName, content); + } + + public async Task> JoinChannelAsync(string serverUrl, string channelName) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + throw new InvalidOperationException("Not connected to server"); + } + + if (entry.Manager.TrackChannel(channelName)) + { + List history = await entry.Manager.JoinChannelAsync(channelName); + return history.Select(m => MessageModelFromDto(m, entry)).ToList(); + } + + List existing = await entry.Manager.GetHistoryAsync(channelName); + return existing.Select(m => MessageModelFromDto(m, entry)).ToList(); + } + + public async Task LeaveChannelAsync(string serverUrl, string channelName) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.Manager.LeaveChannelAsync(channelName); + } + + public async Task> GetHistoryAsync(string serverUrl, string channelName, int count = HubConstants.DefaultHistoryCount, int offset = 0) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return []; + } + + List history = await entry.Manager.GetHistoryAsync(channelName, count, offset); + return history.Select(m => MessageModelFromDto(m, entry)).ToList(); + } + + public async Task> GetOnlineUsersAsync(string serverUrl, string channelName) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return []; + } + + return await entry.Manager.GetOnlineUsersAsync(channelName); + } + + public async Task UpdateStatusAsync(string serverUrl, UserStatus status, string? statusMessage) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.Manager.UpdateStatusAsync(status, statusMessage); + entry.User.Status = status; + entry.User.StatusMessage = statusMessage; + } + + public async Task CreateChannelAsync(string serverUrl, string name, string? topic, bool isPublic) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return null; + } + + return await entry.ApiClient.CreateChannelAsync(name, topic, isPublic); + } + + public async Task DeleteChannelAsync(string serverUrl, string channelName) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.DeleteChannelAsync(channelName); + entry.Manager.UntrackChannel(channelName); + } + + public async Task KickUserAsync(string serverUrl, string username, string? reason) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.KickUserAsync(username, reason); + } + + public async Task BanUserAsync(string serverUrl, string username, string? reason) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.BanUserAsync(username, reason); + } + + public async Task UnbanUserAsync(string serverUrl, string username) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.UnbanUserAsync(username); + } + + public async Task MuteUserAsync(string serverUrl, string username, int? durationMinutes) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.MuteUserAsync(username, durationMinutes); + } + + public async Task UnmuteUserAsync(string serverUrl, string username) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.UnmuteUserAsync(username); + } + + public async Task AssignRoleAsync(string serverUrl, string username, ServerRole role) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.AssignRoleAsync(username, role); + } + + public async Task NukeChannelAsync(string serverUrl, string channelName) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await entry.ApiClient.NukeChannelAsync(channelName); + } + + public async Task UpdateProfileAsync(string serverUrl, string? displayName, string? bio, string? nickColor) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + _ = await entry.ApiClient.UpdateProfileAsync(new UpdateProfileRequest(displayName, bio, nickColor)); + } + + public async Task SetAvatarAsync(string serverUrl, string target) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + _ = await AvatarHelper.UploadAsync(entry.ApiClient, target); + } + + public async Task SendUrlAsync(string serverUrl, string channelName, string url, string? size) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + _ = await entry.ApiClient.SendUrlAsync(channelName, url, size); + } + + public async Task UploadFileAsync(string serverUrl, string channelName, string filePath, string? size) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + await using FileStream stream = File.OpenRead(filePath); + string fileName = Path.GetFileName(filePath); + _ = await entry.ApiClient.UploadFileAsync(channelName, stream, fileName, size); + } + + public void UpdateChannelTopic(string serverUrl, string channelName, string? topic) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + ChannelModel? channel = entry.Server.Channels.FirstOrDefault(c => + string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase)); + _ = channel?.Topic = topic; + } + + public void AddChannelToList(string serverUrl, ChannelDto channelDto) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + ChannelModel model = ChannelModelFromDto(channelDto); + entry.Server.Channels.Add(model); + ChannelAdded?.Invoke(serverUrl, model); + } + + public void RemoveChannelFromList(string serverUrl, string channelName) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + ChannelModel? channel = entry.Server.Channels.FirstOrDefault(c => + string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase)); + if (channel is not null) + { + _ = entry.Server.Channels.Remove(channel); + ChannelRemoved?.Invoke(serverUrl, channelName); + } + } + + public string? GetRefreshToken(string serverUrl) + { + return _connections.TryGetValue(serverUrl, out ServerConnection? entry) + ? entry.ApiClient.RefreshToken + : null; + } + + public async Task DownloadAttachmentAsync(string serverUrl, string relativeUrl, string fileName) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return null; + } + + try + { + return await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, fileName); + } + catch + { + return null; + } + } + + public async Task DownloadImageBytesAsync(string serverUrl, string relativeUrl) + { + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return null; + } + + try + { + string? tempPath = await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, "image"); + if (tempPath is null) + { + return null; + } + + byte[] bytes = await File.ReadAllBytesAsync(tempPath); + try + { + File.Delete(tempPath); + } + catch + { + // Ignore if deletion fails + } + return bytes; + } + catch + { + return null; + } + } + + public string? GetCurrentUsername(string serverUrl) + { + return _connections.TryGetValue(serverUrl, out ServerConnection? entry) + ? entry.User.DisplayName + : null; + } + + public void Dispose() + { + foreach ((string _, ServerConnection? entry) in _connections) + { + entry.ApiClient.Dispose(); + } + _connections.Clear(); + } + + internal static ChannelModel ChannelModelFromDto(ChannelDto dto) + { + return new ChannelModel( + dto.Id.ToString(), + dto.Name, + [], + dto.Topic, + dto.IsPublic); + } + + internal static MessageModel MessageModelFromDto(MessageDto dto, ServerConnection entry) + { + UserModel author = new UserModel( + dto.SenderUsername, + dto.SenderUsername, + dto.SenderNicknameColor); + + return new MessageModel( + dto.Id.ToString("N"), + author, + dto.SentAt, + dto.Content, + dto.ChannelName, + entry.Server.ServerUrl, + dto.Type, + dto.AttachmentUrl, + dto.AttachmentFileName, + dto.AttachmentFileSize); + } + + internal ServerConnection? GetConnection(string serverUrl) + { + return _connections.TryGetValue(serverUrl, out ServerConnection? conn) ? conn : null; + } + private void SaveRefreshToken(string serverUrl, bool rememberMe) { - if (!rememberMe) return; - if (!_connections.TryGetValue(serverUrl, out var entry)) return; - var token = entry.ApiClient.RefreshToken; - if (string.IsNullOrEmpty(token)) return; + if (!rememberMe) + { + return; + } - var config = ConfigManager.Load(); - var saved = config.SavedServers.FirstOrDefault(s => + if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) + { + return; + } + + string? token = entry.ApiClient.RefreshToken; + if (string.IsNullOrEmpty(token)) + { + return; + } + + ClientConfig config = ConfigManager.Load(); + SavedServer? saved = config.SavedServers.FirstOrDefault(s => string.Equals(s.Url, serverUrl, StringComparison.OrdinalIgnoreCase)); if (saved is null) { @@ -149,280 +536,11 @@ public sealed class ConnectionService : IDisposable ConfigManager.Save(config); } - public async Task DisconnectAsync(string serverUrl) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - - entry.Server.IsConnected = false; - entry.Server.IsConnecting = false; - ServerStateChanged?.Invoke(entry.Server); - - await entry.Manager.CleanupAsync(); - entry.ApiClient.Dispose(); - await entry.Manager.DisposeAsync(); - - _connections.Remove(serverUrl); - ServerRemoved?.Invoke(serverUrl); - } - - public async Task SendMessageAsync(string serverUrl, string channelName, string content) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - throw new InvalidOperationException("Not connected to server"); - - await entry.Manager.SendMessageAsync(channelName, content); - } - - public async Task> JoinChannelAsync(string serverUrl, string channelName) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - throw new InvalidOperationException("Not connected to server"); - - if (entry.Manager.TrackChannel(channelName)) - { - var history = await entry.Manager.JoinChannelAsync(channelName); - return history.Select(m => MessageModelFromDto(m, entry)).ToList(); - } - - var existing = await entry.Manager.GetHistoryAsync(channelName); - return existing.Select(m => MessageModelFromDto(m, entry)).ToList(); - } - - public async Task LeaveChannelAsync(string serverUrl, string channelName) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.Manager.LeaveChannelAsync(channelName); - } - - public async Task> GetHistoryAsync(string serverUrl, string channelName, int count = HubConstants.DefaultHistoryCount, int offset = 0) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return []; - - var history = await entry.Manager.GetHistoryAsync(channelName, count, offset); - return history.Select(m => MessageModelFromDto(m, entry)).ToList(); - } - - public async Task> GetOnlineUsersAsync(string serverUrl, string channelName) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return []; - return await entry.Manager.GetOnlineUsersAsync(channelName); - } - - public async Task UpdateStatusAsync(string serverUrl, UserStatus status, string? statusMessage) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.Manager.UpdateStatusAsync(status, statusMessage); - entry.User.Status = status; - entry.User.StatusMessage = statusMessage; - } - - public async Task CreateChannelAsync(string serverUrl, string name, string? topic, bool isPublic) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return null; - return await entry.ApiClient.CreateChannelAsync(name, topic, isPublic); - } - - public async Task DeleteChannelAsync(string serverUrl, string channelName) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.DeleteChannelAsync(channelName); - entry.Manager.UntrackChannel(channelName); - } - - public async Task KickUserAsync(string serverUrl, string username, string? reason) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.KickUserAsync(username, reason); - } - - public async Task BanUserAsync(string serverUrl, string username, string? reason) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.BanUserAsync(username, reason); - } - - public async Task UnbanUserAsync(string serverUrl, string username) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.UnbanUserAsync(username); - } - - public async Task MuteUserAsync(string serverUrl, string username, int? durationMinutes) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.MuteUserAsync(username, durationMinutes); - } - - public async Task UnmuteUserAsync(string serverUrl, string username) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.UnmuteUserAsync(username); - } - - public async Task AssignRoleAsync(string serverUrl, string username, ServerRole role) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.AssignRoleAsync(username, role); - } - - public async Task NukeChannelAsync(string serverUrl, string channelName) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.NukeChannelAsync(channelName); - } - - public async Task UpdateProfileAsync(string serverUrl, string? displayName, string? bio, string? nickColor) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.UpdateProfileAsync(new UpdateProfileRequest(displayName, bio, nickColor)); - } - - public async Task SetAvatarAsync(string serverUrl, string target) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await AvatarHelper.UploadAsync(entry.ApiClient, target); - } - - public async Task SendUrlAsync(string serverUrl, string channelName, string url, string? size) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - await entry.ApiClient.SendUrlAsync(channelName, url, size); - } - - public async Task UploadFileAsync(string serverUrl, string channelName, string filePath, string? size) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - - await using var stream = File.OpenRead(filePath); - var fileName = Path.GetFileName(filePath); - await entry.ApiClient.UploadFileAsync(channelName, stream, fileName, size); - } - - public void UpdateChannelTopic(string serverUrl, string channelName, string? topic) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - - var channel = entry.Server.Channels.FirstOrDefault(c => - string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase)); - if (channel is not null) - channel.Topic = topic; - } - - public void AddChannelToList(string serverUrl, ChannelDto channelDto) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - - var model = ChannelModelFromDto(channelDto); - entry.Server.Channels.Add(model); - ChannelAdded?.Invoke(serverUrl, model); - } - - public void RemoveChannelFromList(string serverUrl, string channelName) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return; - - var channel = entry.Server.Channels.FirstOrDefault(c => - string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase)); - if (channel is not null) - { - entry.Server.Channels.Remove(channel); - ChannelRemoved?.Invoke(serverUrl, channelName); - } - } - - public ClientConfig LoadConfig() => ConfigManager.Load(); - - public void SaveConfig(ClientConfig config) => ConfigManager.Save(config); - - public void SaveServerToConfig(SavedServer server) => ConfigManager.SaveServer(server); - - public void RemoveServerFromConfig(string url) => ConfigManager.RemoveServer(url); - - public CommandHandler CreateCommandHandler() => new(); - - public bool IsCommand(string input) => input.StartsWith('/'); - - public string? GetRefreshToken(string serverUrl) - { - return _connections.TryGetValue(serverUrl, out var entry) - ? entry.ApiClient.RefreshToken - : null; - } - - public async Task DownloadAttachmentAsync(string serverUrl, string relativeUrl, string fileName) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return null; - try - { - return await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, fileName); - } - catch - { - return null; - } - } - - public async Task DownloadImageBytesAsync(string serverUrl, string relativeUrl) - { - if (!_connections.TryGetValue(serverUrl, out var entry)) - return null; - try - { - var tempPath = await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, "image"); - if (tempPath is null) return null; - var bytes = await File.ReadAllBytesAsync(tempPath); - try { File.Delete(tempPath); } catch { } - return bytes; - } - catch - { - return null; - } - } - - public string? GetCurrentUsername(string serverUrl) - { - return _connections.TryGetValue(serverUrl, out var entry) - ? entry.User.DisplayName - : null; - } - - public void Dispose() - { - foreach (var (_, entry) in _connections) - { - entry.ApiClient.Dispose(); - } - _connections.Clear(); - } - private void WireConnectionEvents(ServerConnection entry, ConnectionManager conn) { conn.MessageReceived += message => { - var msg = MessageModelFromDto(message, entry); + MessageModel msg = MessageModelFromDto(message, entry); MessageReceived?.Invoke(entry.Server.ServerUrl, msg); }; @@ -444,12 +562,9 @@ public sealed class ConnectionService : IDisposable conn.ChannelUpdated += channel => { - var existing = entry.Server.Channels.FirstOrDefault(c => + ChannelModel? existing = entry.Server.Channels.FirstOrDefault(c => string.Equals(c.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); - if (existing is not null) - { - existing.Topic = channel.Topic; - } + _ = existing?.Topic = channel.Topic; }; conn.ForceDisconnected += reason => @@ -471,44 +586,14 @@ public sealed class ConnectionService : IDisposable ServerStateChanged?.Invoke(entry.Server); }; } - - internal static ChannelModel ChannelModelFromDto(ChannelDto dto) - { - return new ChannelModel( - dto.Id.ToString(), - dto.Name, - new ObservableCollection(), - dto.Topic, - dto.IsPublic); - } - - internal static MessageModel MessageModelFromDto(MessageDto dto, ServerConnection entry) - { - var author = new UserModel( - dto.SenderUsername, - dto.SenderUsername, - dto.SenderNicknameColor); - - return new MessageModel( - dto.Id.ToString("N"), - author, - dto.SentAt, - dto.Content, - dto.ChannelName, - entry.Server.ServerUrl, - dto.Type, - dto.AttachmentUrl, - dto.AttachmentFileName, - dto.AttachmentFileSize); - } } internal sealed class ServerConnection { - internal ConnectionManager Manager { get; } public ApiClient ApiClient { get; } public ServerModel Server { get; } public UserModel User { get; } + internal ConnectionManager Manager { get; } internal ServerConnection(ConnectionManager manager, ApiClient apiClient, ServerModel server, UserModel user) { diff --git a/Decho/ViewLocator.cs b/Decho/ViewLocator.cs index 36bcb58..4de6614 100644 --- a/Decho/ViewLocator.cs +++ b/Decho/ViewLocator.cs @@ -3,10 +3,10 @@ using Avalonia.Controls.Templates; using Decho.ViewModels; -using System; using System.Diagnostics.CodeAnalysis; namespace Decho; + /// /// Given a view model, returns the corresponding view if possible. /// @@ -18,10 +18,12 @@ public class ViewLocator : IDataTemplate public Control? Build(object? param) { if (param is null) + { return null; + } - var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); + string name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + Type? type = Type.GetType(name); if (type != null) { @@ -35,4 +37,4 @@ public class ViewLocator : IDataTemplate { return data is ViewModelBase; } -} +} \ No newline at end of file diff --git a/Decho/ViewModels/ChannelViewModel.cs b/Decho/ViewModels/ChannelViewModel.cs index b9dfaf1..b0735fd 100644 --- a/Decho/ViewModels/ChannelViewModel.cs +++ b/Decho/ViewModels/ChannelViewModel.cs @@ -1,20 +1,12 @@ -using System.Collections.ObjectModel; -using System.Linq; - using Decho.Models; +using System.Collections.ObjectModel; + namespace Decho.ViewModels; -public sealed class ChannelViewModel : ViewModelBase +public sealed class ChannelViewModel(ChannelModel model) : ViewModelBase { - public ChannelViewModel(ChannelModel model) - { - Model = model; - Messages = new ObservableCollection( - model.Messages.Select(message => new MessageViewModel(message))); - } - - public ChannelModel Model { get; } + public ChannelModel Model { get; } = model; public string Name => Model.Name; @@ -36,7 +28,8 @@ public sealed class ChannelViewModel : ViewModelBase public bool IsPublic => Model.IsPublic; - public ObservableCollection Messages { get; } + public ObservableCollection Messages { get; } = new ObservableCollection( + model.Messages.Select(message => new MessageViewModel(message))); public void ClearMessages() { @@ -49,9 +42,4 @@ public sealed class ChannelViewModel : ViewModelBase Model.Messages.Add(message); Messages.Add(new MessageViewModel(message)); } - - public void AddMessageViewModel(MessageViewModel messageViewModel) - { - Messages.Add(messageViewModel); - } } \ No newline at end of file diff --git a/Decho/ViewModels/ChatViewModel.cs b/Decho/ViewModels/ChatViewModel.cs index 2d56766..c6c47c3 100644 --- a/Decho/ViewModels/ChatViewModel.cs +++ b/Decho/ViewModels/ChatViewModel.cs @@ -1,72 +1,60 @@ -using System; -using System.Collections.ObjectModel; -using System.Threading.Tasks; - using EchoHub.Client.Commands; -using ReactiveUI; + +using System.Collections.ObjectModel; namespace Decho.ViewModels; public sealed class ChatViewModel : ViewModelBase { - private ObservableCollection _messages = new(); - private string _channelTitle = "Select a channel"; - private string? _channelTopic; - private bool _hasTopic; - private string _currentServerUrl = string.Empty; - private string _currentChannelName = string.Empty; - - public ChatViewModel() - { - Composer = new MessageComposerViewModel(); - Composer.CommandRequested += HandleCommandAsync; - } - public ObservableCollection Messages { - get => _messages; - private set => this.RaiseAndSetIfChanged(ref _messages, value); - } + get; + private set => this.RaiseAndSetIfChanged(ref field, value); + } = []; public string ChannelTitle { - get => _channelTitle; - private set => this.RaiseAndSetIfChanged(ref _channelTitle, value); - } + get; + private set => this.RaiseAndSetIfChanged(ref field, value); + } = "Select a channel"; public string? ChannelTopic { - get => _channelTopic; + get; set { - this.RaiseAndSetIfChanged(ref _channelTopic, value); + _ = this.RaiseAndSetIfChanged(ref field, value); this.RaisePropertyChanged(nameof(HasTopic)); } } public bool HasTopic { - get => _hasTopic; - private set => this.RaiseAndSetIfChanged(ref _hasTopic, value); + get; + private set => this.RaiseAndSetIfChanged(ref field, value); } public MessageComposerViewModel Composer { get; } - public string CurrentServerUrl => _currentServerUrl; - public string CurrentChannelName => _currentChannelName; + public string CurrentServerUrl { get; private set; } = string.Empty; - public event Func>? CommandRequested; + public string CurrentChannelName { get; private set; } = string.Empty; + + public ChatViewModel() + { + Composer = new MessageComposerViewModel(); + } public void SetChannel(ChannelViewModel? channel, string serverUrl = "") { if (channel is null) { - Messages = new ObservableCollection(); + Messages = []; ChannelTitle = "Select a channel"; ChannelTopic = null; HasTopic = false; - _currentChannelName = string.Empty; - _currentServerUrl = string.Empty; + CurrentChannelName = string.Empty; + CurrentServerUrl = string.Empty; Composer.SetServer(string.Empty); return; } @@ -75,8 +63,8 @@ public sealed class ChatViewModel : ViewModelBase ChannelTitle = "#" + channel.Name; ChannelTopic = channel.Topic; HasTopic = channel.HasTopic; - _currentChannelName = channel.Name; - _currentServerUrl = serverUrl; + CurrentChannelName = channel.Name; + CurrentServerUrl = serverUrl; Composer.SetServer(serverUrl); } @@ -85,25 +73,11 @@ public sealed class ChatViewModel : ViewModelBase Composer.SetCommandHandler(handler); } - public void AddMessage(MessageViewModel message) - { - Messages.Add(message); - } - public void ClearMessages() { - Messages = new ObservableCollection(); + Messages = []; ChannelTitle = "Select a channel"; ChannelTopic = null; HasTopic = false; } - - private async Task HandleCommandAsync(string commandText) - { - if (CommandRequested is not null) - { - return await CommandRequested(commandText); - } - return null; - } } \ No newline at end of file diff --git a/Decho/ViewModels/MainWindowViewModel.cs b/Decho/ViewModels/MainWindowViewModel.cs index c53ccf3..141255d 100644 --- a/Decho/ViewModels/MainWindowViewModel.cs +++ b/Decho/ViewModels/MainWindowViewModel.cs @@ -1,14 +1,4 @@ -using System; -using System.Collections.ObjectModel; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using System.Reactive.Concurrency; -using System.Reactive.Linq; -using System.Threading.Tasks; - using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; using Decho.Models; using Decho.Services; @@ -19,16 +9,14 @@ using EchoHub.Core.Constants; using EchoHub.Core.DTOs; using EchoHub.Core.Models; -using ReactiveUI; +using System.Reactive; +using System.Reactive.Linq; namespace Decho.ViewModels; public sealed class MainWindowViewModel : ViewModelBase { - private readonly ConnectionService _connectionService; private readonly CommandHandler _commandHandler; - private string _statusText = "Ready"; - private bool _isConnected; private Window? _mainWindow; public string Title { get; } = "Decho"; @@ -39,24 +27,18 @@ public sealed class MainWindowViewModel : ViewModelBase public string StatusText { - get => _statusText; - set => this.RaiseAndSetIfChanged(ref _statusText, value); - } + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } = "Ready"; - public bool IsConnected - { - get => _isConnected; - set => this.RaiseAndSetIfChanged(ref _isConnected, value); - } - - public ConnectionService ConnectionService => _connectionService; + public ConnectionService ConnectionService { get; } public ReactiveCommand AddServerCommand { get; } public MainWindowViewModel() { - _connectionService = new ConnectionService(); - _commandHandler = _connectionService.CreateCommandHandler(); + ConnectionService = new ConnectionService(); + _commandHandler = new CommandHandler(); Sidebar = new SidebarViewModel(); Chat = new ChatViewModel(); @@ -70,8 +52,8 @@ public sealed class MainWindowViewModel : ViewModelBase WireCommandHandlerEvents(); WireConnectionServiceEvents(); - Sidebar.WhenAnyValue(x => x.SelectedChannel) - .Subscribe(channel => HandleChannelSelected(channel)); + _ = Sidebar.WhenAnyValue(x => x.SelectedChannel) + .Subscribe(HandleChannelSelected); _ = InitializeSavedServersAsync(); } @@ -81,15 +63,20 @@ public sealed class MainWindowViewModel : ViewModelBase _mainWindow = window; } + public void Dispose() + { + ConnectionService.Dispose(); + } + private void AddServer() { - var placeholderServer = new ServerModel( + ServerModel placeholderServer = new ServerModel( Guid.NewGuid().ToString("N"), "New Server", - new ObservableCollection(), + [], isConnected: false); - var serverVm = new ServerViewModel(placeholderServer); + ServerViewModel serverVm = new ServerViewModel(placeholderServer); serverVm.ConnectRequested += async () => await HandleServerConnectRequested(serverVm); serverVm.DisconnectRequested += async () => await HandleServerDisconnectRequested(serverVm); @@ -101,9 +88,13 @@ public sealed class MainWindowViewModel : ViewModelBase { _commandHandler.OnSetStatus += async (status, message) => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.UpdateStatusAsync(serverUrl, status, message); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.UpdateStatusAsync(serverUrl, status, message); }; _commandHandler.OnSetTheme += themeName => @@ -113,111 +104,156 @@ public sealed class MainWindowViewModel : ViewModelBase _commandHandler.OnJoinChannel += async channelName => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } - var channel = await _connectionService.JoinChannelAsync(serverUrl, channelName); + List channel = await ConnectionService.JoinChannelAsync(serverUrl, channelName); EnsureChannelInList(serverUrl, channelName); - var channelModel = FindChannel(serverUrl, channelName); + ChannelModel? channelModel = FindChannel(serverUrl, channelName); if (channelModel is not null) { - var channelVm = Sidebar.GetServer(serverUrl)?.Channels + ChannelViewModel? channelVm = Sidebar.GetServer(serverUrl)?.Channels .FirstOrDefault(c => c.Name == channelName); if (channelVm is not null) { - foreach (var msg in channel) + foreach (MessageModel msg in channel) + { channelVm.AddMessage(msg); + } } } }; _commandHandler.OnLeaveChannel += async () => { - var serverUrl = GetCurrentServerUrl(); - var channel = Chat.CurrentChannelName; - if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return; + string serverUrl = GetCurrentServerUrl(); + string channel = Chat.CurrentChannelName; + if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) + { + return; + } - if (channel == HubConstants.DefaultChannel) return; - await _connectionService.LeaveChannelAsync(serverUrl, channel); + if (channel == HubConstants.DefaultChannel) + { + return; + } + + await ConnectionService.LeaveChannelAsync(serverUrl, channel); }; _commandHandler.OnListUsers += async () => { - var serverUrl = GetCurrentServerUrl(); - var channel = Chat.CurrentChannelName; - if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return; + string serverUrl = GetCurrentServerUrl(); + string channel = Chat.CurrentChannelName; + if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) + { + return; + } - var users = await _connectionService.GetOnlineUsersAsync(serverUrl, channel); - var userList = string.Join(", ", users.Select(u => u.DisplayName ?? u.Username)); + List users = await ConnectionService.GetOnlineUsersAsync(serverUrl, channel); + string userList = string.Join(", ", users.Select(u => u.DisplayName ?? u.Username)); StatusText = $"Online in #{channel}: {userList}"; }; _commandHandler.OnSetTopic += async topic => { - var serverUrl = GetCurrentServerUrl(); - var channel = Chat.CurrentChannelName; - if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return; + string serverUrl = GetCurrentServerUrl(); + string channel = Chat.CurrentChannelName; + if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) + { + return; + } - await _connectionService.UpdateProfileAsync(serverUrl, null, null, null); - _connectionService.UpdateChannelTopic(serverUrl, channel, topic); + await ConnectionService.UpdateProfileAsync(serverUrl, null, null, null); + ConnectionService.UpdateChannelTopic(serverUrl, channel, topic); Chat.ChannelTopic = topic; }; _commandHandler.OnKickUser += async (username, reason) => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.KickUserAsync(serverUrl, username, reason); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.KickUserAsync(serverUrl, username, reason); }; _commandHandler.OnBanUser += async (username, reason) => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.BanUserAsync(serverUrl, username, reason); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.BanUserAsync(serverUrl, username, reason); }; _commandHandler.OnUnbanUser += async username => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.UnbanUserAsync(serverUrl, username); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.UnbanUserAsync(serverUrl, username); }; _commandHandler.OnMuteUser += async (username, duration) => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.MuteUserAsync(serverUrl, username, duration); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.MuteUserAsync(serverUrl, username, duration); }; _commandHandler.OnUnmuteUser += async username => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.UnmuteUserAsync(serverUrl, username); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.UnmuteUserAsync(serverUrl, username); }; _commandHandler.OnAssignRole += async (username, roleStr) => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } - var role = roleStr.ToLowerInvariant() switch + ServerRole role = roleStr.ToLowerInvariant() switch { "admin" => ServerRole.Admin, "mod" => ServerRole.Mod, _ => ServerRole.Member, }; - await _connectionService.AssignRoleAsync(serverUrl, username, role); + await ConnectionService.AssignRoleAsync(serverUrl, username, role); }; _commandHandler.OnNukeChannel += async () => { - var serverUrl = GetCurrentServerUrl(); - var channel = Chat.CurrentChannelName; - if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return; - await _connectionService.NukeChannelAsync(serverUrl, channel); + string serverUrl = GetCurrentServerUrl(); + string channel = Chat.CurrentChannelName; + if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) + { + return; + } + + await ConnectionService.NukeChannelAsync(serverUrl, channel); }; _commandHandler.OnTestSound += () => Task.CompletedTask; @@ -225,7 +261,10 @@ public sealed class MainWindowViewModel : ViewModelBase _commandHandler.OnQuit += () => { if (_mainWindow is not null) + { Avalonia.Threading.Dispatcher.UIThread.Post(() => _mainWindow.Close()); + } + return Task.CompletedTask; }; @@ -233,20 +272,23 @@ public sealed class MainWindowViewModel : ViewModelBase _commandHandler.OnSendFile += async (target, size) => { - var serverUrl = GetCurrentServerUrl(); - var channel = Chat.CurrentChannelName; - if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return; + string serverUrl = GetCurrentServerUrl(); + string channel = Chat.CurrentChannelName; + if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) + { + return; + } try { - if (Uri.TryCreate(target, UriKind.Absolute, out var uri) + if (Uri.TryCreate(target, UriKind.Absolute, out Uri? uri) && (uri.Scheme == "http" || uri.Scheme == "https")) { - await _connectionService.SendUrlAsync(serverUrl, channel, target, size); + await ConnectionService.SendUrlAsync(serverUrl, channel, target, size); } else { - await _connectionService.UploadFileAsync(serverUrl, channel, target, size); + await ConnectionService.UploadFileAsync(serverUrl, channel, target, size); } } catch (Exception ex) @@ -257,23 +299,35 @@ public sealed class MainWindowViewModel : ViewModelBase _commandHandler.OnSetNick += async displayName => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.UpdateProfileAsync(serverUrl, displayName, null, null); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.UpdateProfileAsync(serverUrl, displayName, null, null); }; _commandHandler.OnSetColor += async color => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.UpdateProfileAsync(serverUrl, null, null, color); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.UpdateProfileAsync(serverUrl, null, null, color); }; _commandHandler.OnSetAvatar += async target => { - var serverUrl = GetCurrentServerUrl(); - if (string.IsNullOrEmpty(serverUrl)) return; - await _connectionService.SetAvatarAsync(serverUrl, target); + string serverUrl = GetCurrentServerUrl(); + if (string.IsNullOrEmpty(serverUrl)) + { + return; + } + + await ConnectionService.SetAvatarAsync(serverUrl, target); }; _commandHandler.OnOpenProfile += async username => @@ -283,8 +337,8 @@ public sealed class MainWindowViewModel : ViewModelBase _commandHandler.OnOpenServers += () => { - var config = _connectionService.LoadConfig(); - var servers = string.Join("\n", config.SavedServers.Select(s => + ClientConfig config = ConfigManager.Load(); + string servers = string.Join("\n", config.SavedServers.Select(s => $"{s.Name} ({s.Url}) - {s.Username ?? "?"}")); StatusText = servers; return Task.CompletedTask; @@ -293,75 +347,67 @@ public sealed class MainWindowViewModel : ViewModelBase private void WireConnectionServiceEvents() { - _connectionService.ServerAdded += server => + ConnectionService.ServerAdded += server => { - var serverVm = new ServerViewModel(server); + ServerViewModel serverVm = new ServerViewModel(server); serverVm.ConnectRequested += () => HandleServerConnectRequested(serverVm); serverVm.DisconnectRequested += () => HandleServerDisconnectRequested(serverVm); - serverVm.WhenAnyValue(s => s.SelectedChannel) + _ = serverVm.WhenAnyValue(s => s.SelectedChannel) .Where(channel => channel is not null) .Subscribe(channel => Sidebar.SelectedChannel = channel!); Avalonia.Threading.Dispatcher.UIThread.Post(() => { - var existing = Sidebar.GetServer(server.ServerUrl); + ServerViewModel? existing = Sidebar.GetServer(server.ServerUrl); if (existing is not null) - Sidebar.Servers.Remove(existing); + { + _ = Sidebar.Servers.Remove(existing); + } + Sidebar.Servers.Add(serverVm); StatusText = $"Connected to {server.Name}"; - IsConnected = true; }); }; - _connectionService.ServerRemoved += serverUrl => + ConnectionService.ServerRemoved += serverUrl => { Avalonia.Threading.Dispatcher.UIThread.Post(() => { Sidebar.RemoveServer(serverUrl); if (Sidebar.Servers.Count == 0) { - IsConnected = false; Chat.ClearMessages(); StatusText = "Ready"; } }); }; - _connectionService.ServerStateChanged += server => + ConnectionService.ServerStateChanged += server => { Avalonia.Threading.Dispatcher.UIThread.Post(() => { - var serverVm = Sidebar.GetServer(server.ServerUrl); + ServerViewModel? serverVm = Sidebar.GetServer(server.ServerUrl); if (serverVm is not null) { serverVm.SyncFromModel(); if (!server.IsConnected) + { StatusText = $"Disconnected from {server.Name}"; + } } }); }; - _connectionService.MessageReceived += (serverUrl, message) => + ConnectionService.MessageReceived += (serverUrl, message) => { Avalonia.Threading.Dispatcher.UIThread.Post(() => { - var channelVm = FindChannelViewModel(serverUrl, message.ChannelName); - if (channelVm is not null) - { - channelVm.AddMessage(message); - } + ChannelViewModel? channelVm = FindChannelViewModel(serverUrl, message.ChannelName); + channelVm?.AddMessage(message); }); }; - _connectionService.ChannelAdded += (serverUrl, channel) => - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - EnsureChannelInList(serverUrl, channel.Name); - }); - }; - - _connectionService.ErrorOccurred += (serverUrl, error) => + ConnectionService.ErrorOccurred += (serverUrl, error) => { Avalonia.Threading.Dispatcher.UIThread.Post(() => { @@ -372,10 +418,10 @@ public sealed class MainWindowViewModel : ViewModelBase private async Task HandleCommandAsync(string commandText) { - var result = await _commandHandler.HandleAsync(commandText); + CommandResult result = await _commandHandler.HandleAsync(commandText); if (result.Message is not null && !result.IsError) { - Chat.AddMessage(new MessageViewModel(new MessageModel( + Chat.Messages.Add(new MessageViewModel(new MessageModel( Guid.NewGuid().ToString("N"), new UserModel("system", "System"), DateTimeOffset.Now, @@ -389,19 +435,21 @@ public sealed class MainWindowViewModel : ViewModelBase private void HandleSendRequested(string serverUrl, string text) { if (string.IsNullOrEmpty(Chat.CurrentChannelName)) + { return; + } - if (_commandHandler.IsCommand(text)) - { - _ = HandleCommandAsync(text); - return; - } + if (_commandHandler.IsCommand(text)) + { + _ = HandleCommandAsync(text); + return; + } - Task.Run(async () => + _ = Task.Run(async () => { try { - await _connectionService.SendMessageAsync(serverUrl, Chat.CurrentChannelName, text); + await ConnectionService.SendMessageAsync(serverUrl, Chat.CurrentChannelName, text); } catch (Exception ex) { @@ -415,13 +463,16 @@ public sealed class MainWindowViewModel : ViewModelBase private void HandleFileUploadRequested(string serverUrl, string filePath) { - if (string.IsNullOrEmpty(Chat.CurrentChannelName)) return; + if (string.IsNullOrEmpty(Chat.CurrentChannelName)) + { + return; + } - Task.Run(async () => + _ = Task.Run(async () => { try { - await _connectionService.UploadFileAsync(serverUrl, Chat.CurrentChannelName, filePath, null); + await ConnectionService.UploadFileAsync(serverUrl, Chat.CurrentChannelName, filePath, null); Avalonia.Threading.Dispatcher.UIThread.Post(() => StatusText = "File uploaded"); } @@ -441,24 +492,25 @@ public sealed class MainWindowViewModel : ViewModelBase return; } - var serverUrl = FindServerUrlForChannel(channel); + string serverUrl = FindServerUrlForChannel(channel); Chat.SetChannel(channel, serverUrl); if (!string.IsNullOrEmpty(serverUrl)) { - var commandHandler = _connectionService.CreateCommandHandler(); - Chat.SetComposerCommandHandler(commandHandler); + Chat.SetComposerCommandHandler(new CommandHandler()); - Task.Run(async () => + _ = Task.Run(async () => { try { - var history = await _connectionService.JoinChannelAsync(serverUrl, channel.Name); + List history = await ConnectionService.JoinChannelAsync(serverUrl, channel.Name); Avalonia.Threading.Dispatcher.UIThread.Post(() => { channel.ClearMessages(); - foreach (var msg in history) + foreach (MessageModel msg in history) + { channel.AddMessage(msg); + } }); } catch @@ -471,19 +523,19 @@ public sealed class MainWindowViewModel : ViewModelBase private async Task InitializeSavedServersAsync() { - var config = _connectionService.LoadConfig(); - var connectTasks = new List(); + ClientConfig config = ConfigManager.Load(); + List connectTasks = []; - foreach (var saved in config.SavedServers) + foreach (SavedServer saved in config.SavedServers) { - var placeholderServer = new ServerModel( + ServerModel placeholderServer = new ServerModel( Guid.NewGuid().ToString("N"), saved.Name, - new ObservableCollection(), + [], saved.Url, isConnected: false); - var serverVm = new ServerViewModel(placeholderServer); + ServerViewModel serverVm = new ServerViewModel(placeholderServer); serverVm.ConnectRequested += async () => await HandleServerConnectRequested(serverVm); serverVm.DisconnectRequested += async () => await HandleServerDisconnectRequested(serverVm); @@ -504,12 +556,14 @@ public sealed class MainWindowViewModel : ViewModelBase private async Task AutoConnectSavedServer(ServerViewModel serverVm, SavedServer saved) { if (string.IsNullOrEmpty(saved.Username) || string.IsNullOrEmpty(saved.RefreshToken)) + { return; + } try { serverVm.IsConnecting = true; - await _connectionService.ConnectWithSavedTokenAsync( + await ConnectionService.ConnectWithSavedTokenAsync( saved.Url, saved.Username, saved.RefreshToken, saved.RememberMe); } catch @@ -518,43 +572,24 @@ public sealed class MainWindowViewModel : ViewModelBase } } - private static async Task ShowMessageBox(Window owner, string title, string message) - { - var msgBox = new Window - { - Title = title, - Width = 400, - Height = 180, - WindowStartupLocation = WindowStartupLocation.CenterOwner, - CanResize = false, - }; - - var stack = new StackPanel { Spacing = 10, Margin = new Avalonia.Thickness(15) }; - stack.Children.Add(new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap }); - var okBtn = new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }; - stack.Children.Add(okBtn); - msgBox.Content = stack; - - var tcs = new TaskCompletionSource(); - okBtn.Click += (_, _) => { msgBox.Close(); tcs.TrySetResult(); }; - msgBox.Closed += (_, _) => tcs.TrySetResult(); - - await msgBox.ShowDialog(owner); - await tcs.Task; - } - private async Task HandleServerConnectRequested(ServerViewModel serverVm) { // Show connect dialog - if (_mainWindow is null) return; + if (_mainWindow is null) + { + return; + } - var config = _connectionService.LoadConfig(); - var prefill = config.SavedServers.FirstOrDefault(s => + ClientConfig config = ConfigManager.Load(); + SavedServer? prefill = config.SavedServers.FirstOrDefault(s => string.Equals(s.Url, serverVm.ServerUrl, StringComparison.OrdinalIgnoreCase)); - var dialog = new ConnectDialog(config.SavedServers, prefill); + ConnectDialog dialog = new ConnectDialog(config.SavedServers, prefill); - var result = await dialog.ShowAsync(_mainWindow); - if (result is null) return; + ConnectDialogResult? result = await dialog.ShowAsync(_mainWindow); + if (result is null) + { + return; + } try { @@ -563,12 +598,12 @@ public sealed class MainWindowViewModel : ViewModelBase if (result.IsSavedSession && result.SavedRefreshToken is not null) { - await _connectionService.ConnectWithSavedTokenAsync( + await ConnectionService.ConnectWithSavedTokenAsync( result.ServerUrl, result.Username, result.SavedRefreshToken, result.RememberMe); } else { - await _connectionService.ConnectAsync( + _ = await ConnectionService.ConnectAsync( result.ServerUrl, result.Username, result.Password, result.IsRegister, result.RememberMe); } @@ -576,8 +611,8 @@ public sealed class MainWindowViewModel : ViewModelBase Sidebar.RemoveServer(serverVm.ServerUrl); // Save to config with refresh token - var refreshToken = _connectionService.GetRefreshToken(result.ServerUrl); - var savedServer = new SavedServer + string? refreshToken = ConnectionService.GetRefreshToken(result.ServerUrl); + SavedServer savedServer = new SavedServer { Name = new Uri(result.ServerUrl).Host, Url = result.ServerUrl, @@ -586,7 +621,7 @@ public sealed class MainWindowViewModel : ViewModelBase RememberMe = result.RememberMe, LastConnected = DateTimeOffset.Now, }; - _connectionService.SaveServerToConfig(savedServer); + ConfigManager.SaveServer(savedServer); } catch (Exception ex) { @@ -599,7 +634,7 @@ public sealed class MainWindowViewModel : ViewModelBase { try { - await _connectionService.DisconnectAsync(serverVm.ServerUrl); + await ConnectionService.DisconnectAsync(serverVm.ServerUrl); } catch (Exception ex) { @@ -609,38 +644,43 @@ public sealed class MainWindowViewModel : ViewModelBase private void EnsureChannelInList(string serverUrl, string channelName) { - var serverVm = Sidebar.GetServer(serverUrl); - if (serverVm is null) return; + ServerViewModel? serverVm = Sidebar.GetServer(serverUrl); + if (serverVm is null) + { + return; + } if (!serverVm.Channels.Any(c => c.Name == channelName)) { - var channelModel = new ChannelModel( + ChannelModel channelModel = new ChannelModel( Guid.NewGuid().ToString("N"), channelName, - new System.Collections.ObjectModel.ObservableCollection()); - var channelVm = new ChannelViewModel(channelModel); + []); + ChannelViewModel channelVm = new ChannelViewModel(channelModel); serverVm.Channels.Add(channelVm); } } private ChannelModel? FindChannel(string serverUrl, string channelName) { - var serverVm = Sidebar.GetServer(serverUrl); + ServerViewModel? serverVm = Sidebar.GetServer(serverUrl); return serverVm?.Channels.FirstOrDefault(c => c.Name == channelName)?.Model; } private ChannelViewModel? FindChannelViewModel(string serverUrl, string channelName) { - var serverVm = Sidebar.GetServer(serverUrl); + ServerViewModel? serverVm = Sidebar.GetServer(serverUrl); return serverVm?.Channels.FirstOrDefault(c => c.Name == channelName); } private string FindServerUrlForChannel(ChannelViewModel channel) { - foreach (var server in Sidebar.Servers) + foreach (ServerViewModel server in Sidebar.Servers) { if (server.Channels.Contains(channel)) + { return server.ServerUrl; + } } return string.Empty; } @@ -649,11 +689,6 @@ public sealed class MainWindowViewModel : ViewModelBase { return Chat.CurrentServerUrl; } - - public void Dispose() - { - _connectionService.Dispose(); - } } public sealed class ConnectDialogResult @@ -667,20 +702,14 @@ public sealed class ConnectDialogResult public string? SavedRefreshToken { get; set; } } -public sealed class ConnectDialog +public sealed class ConnectDialog(List savedServers, SavedServer? prefill = null) { - private readonly List _savedServers; - private readonly SavedServer? _prefill; - - public ConnectDialog(List savedServers, SavedServer? prefill = null) - { - _savedServers = savedServers; - _prefill = prefill; - } + private readonly List _savedServers = savedServers; + private readonly SavedServer? _prefill = prefill; public async Task ShowAsync(Window owner) { - var dialog = new Window + Window dialog = new Window { Title = "Connect to Server", Width = 450, @@ -689,31 +718,31 @@ public sealed class ConnectDialog CanResize = false, }; - var stack = new StackPanel { Spacing = 8, Margin = new Avalonia.Thickness(15) }; + StackPanel stack = new StackPanel { Spacing = 8, Margin = new Avalonia.Thickness(15) }; - var urlLabel = new TextBlock { Text = "Server URL:" }; - var urlBox = new TextBox { Watermark = "http://localhost:5000", Text = "http://localhost:5000" }; + TextBlock urlLabel = new TextBlock { Text = "Server URL:" }; + TextBox urlBox = new TextBox { Watermark = "http://localhost:5000", Text = "http://localhost:5000" }; stack.Children.Add(urlLabel); stack.Children.Add(urlBox); - var userLabel = new TextBlock { Text = "Username:" }; - var userBox = new TextBox { Watermark = "username" }; + TextBlock userLabel = new TextBlock { Text = "Username:" }; + TextBox userBox = new TextBox { Watermark = "username" }; stack.Children.Add(userLabel); stack.Children.Add(userBox); - var passLabel = new TextBlock { Text = "Password:" }; - var passBox = new TextBox { Watermark = "password", PasswordChar = '*' }; + TextBlock passLabel = new TextBlock { Text = "Password:" }; + TextBox passBox = new TextBox { Watermark = "password", PasswordChar = '*' }; stack.Children.Add(passLabel); stack.Children.Add(passBox); - var rememberMe = new CheckBox { Content = "Remember me", IsChecked = true }; + CheckBox rememberMe = new CheckBox { Content = "Remember me", IsChecked = true }; stack.Children.Add(rememberMe); - var buttonPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, Spacing = 10 }; + StackPanel buttonPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, Spacing = 10 }; - var loginBtn = new Button { Content = "Login", Width = 100 }; - var registerBtn = new Button { Content = "Register", Width = 100 }; - var cancelBtn = new Button { Content = "Cancel", Width = 100 }; + Button loginBtn = new Button { Content = "Login", Width = 100 }; + Button registerBtn = new Button { Content = "Register", Width = 100 }; + Button cancelBtn = new Button { Content = "Cancel", Width = 100 }; buttonPanel.Children.Add(loginBtn); buttonPanel.Children.Add(registerBtn); buttonPanel.Children.Add(cancelBtn); @@ -722,24 +751,29 @@ public sealed class ConnectDialog ListBox? serversList = null; if (_savedServers.Count > 0) { - var sep = new TextBlock { Text = "--- Saved Servers ---", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }; + TextBlock sep = new TextBlock { Text = "--- Saved Servers ---", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }; stack.Children.Add(sep); - serversList = new ListBox { Height = 100 }; - serversList.ItemsSource = _savedServers.Select(s => $"{s.Name} ({s.Url}) - {s.Username ?? "?"}").ToList(); + serversList = new ListBox + { + Height = 100, + ItemsSource = _savedServers.Select(s => $"{s.Name} ({s.Url}) - {s.Username ?? "?"}").ToList() + }; stack.Children.Add(serversList); serversList.SelectionChanged += (_, _) => { - var idx = serversList.SelectedIndex; + int idx = serversList.SelectedIndex; if (idx >= 0 && idx < _savedServers.Count) { - var s = _savedServers[idx]; + SavedServer s = _savedServers[idx]; urlBox.Text = s.Url; userBox.Text = s.Username ?? ""; rememberMe.IsChecked = s.RememberMe; if (!string.IsNullOrEmpty(s.RefreshToken)) + { passBox.Text = ""; + } } }; } @@ -748,22 +782,24 @@ public sealed class ConnectDialog { urlBox.Text = _prefill.Url; userBox.Text = _prefill.Username ?? ""; - var idx = _savedServers.FindIndex(s => + int idx = _savedServers.FindIndex(s => string.Equals(s.Url, _prefill.Url, StringComparison.OrdinalIgnoreCase)); if (idx >= 0 && idx < _savedServers.Count && serversList is not null) + { serversList.SelectedIndex = idx; + } } dialog.Content = new ScrollViewer { Content = stack }; - var tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new TaskCompletionSource(); ConnectDialogResult? result = null; loginBtn.Click += async (_, _) => { - var url = urlBox.Text?.Trim() ?? ""; - var user = userBox.Text?.Trim() ?? ""; - var pass = passBox.Text ?? ""; + string url = urlBox.Text?.Trim() ?? ""; + string user = userBox.Text?.Trim() ?? ""; + string pass = passBox.Text ?? ""; if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user)) { @@ -772,7 +808,7 @@ public sealed class ConnectDialog } // Check for saved session - var saved = _savedServers.FirstOrDefault(s => + SavedServer? saved = _savedServers.FirstOrDefault(s => string.Equals(s.Url, url, StringComparison.OrdinalIgnoreCase) && string.Equals(s.Username, user, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(s.RefreshToken)); @@ -807,14 +843,14 @@ public sealed class ConnectDialog } dialog.Close(); - tcs.TrySetResult(result); + _ = tcs.TrySetResult(result); }; registerBtn.Click += (_, _) => { - var url = urlBox.Text?.Trim() ?? ""; - var user = userBox.Text?.Trim() ?? ""; - var pass = passBox.Text ?? ""; + string url = urlBox.Text?.Trim() ?? ""; + string user = userBox.Text?.Trim() ?? ""; + string pass = passBox.Text ?? ""; if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass)) { @@ -832,28 +868,32 @@ public sealed class ConnectDialog }; dialog.Close(); - tcs.TrySetResult(result); + _ = tcs.TrySetResult(result); }; cancelBtn.Click += (_, _) => { dialog.Close(); - tcs.TrySetResult(null); + _ = tcs.TrySetResult(null); }; dialog.Closed += (_, _) => tcs.TrySetResult(result); if (owner is not null) + { await dialog.ShowDialog(owner); + } else + { dialog.Show(); + } return await tcs.Task; } private static async Task ShowMessageBox(Window owner, string title, string message) { - var msgBox = new Window + Window msgBox = new Window { Title = title, Width = 350, @@ -862,14 +902,14 @@ public sealed class ConnectDialog CanResize = false, }; - var stack = new StackPanel { Spacing = 10, Margin = new Avalonia.Thickness(15) }; + StackPanel stack = new StackPanel { Spacing = 10, Margin = new Avalonia.Thickness(15) }; stack.Children.Add(new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap }); - var okBtn = new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }; + Button okBtn = new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }; stack.Children.Add(okBtn); msgBox.Content = stack; - var tcs = new TaskCompletionSource(); - okBtn.Click += (_, _) => { msgBox.Close(); tcs.TrySetResult(); }; + TaskCompletionSource tcs = new TaskCompletionSource(); + okBtn.Click += (_, _) => { msgBox.Close(); _ = tcs.TrySetResult(); }; msgBox.Closed += (_, _) => tcs.TrySetResult(); await msgBox.ShowDialog(owner); diff --git a/Decho/ViewModels/MessageComposerViewModel.cs b/Decho/ViewModels/MessageComposerViewModel.cs index fe1942b..6d994c8 100644 --- a/Decho/ViewModels/MessageComposerViewModel.cs +++ b/Decho/ViewModels/MessageComposerViewModel.cs @@ -1,50 +1,48 @@ -using System; -using System.Reactive; -using System.Reactive.Linq; -using System.Threading.Tasks; - using EchoHub.Client.Commands; -using ReactiveUI; + +using System.Reactive; namespace Decho.ViewModels; public sealed class MessageComposerViewModel : ViewModelBase { - private string _draft = string.Empty; - private CommandHandler? _commandHandler; - private string _serverUrl = string.Empty; + public event Action? SendRequested; - public MessageComposerViewModel() - { - var canSend = this.WhenAnyValue(x => x.Draft, draft => !string.IsNullOrWhiteSpace(draft)); - SendCommand = ReactiveCommand.Create(Send, canSend); - } + public event Func>? CommandRequested; + + public event Action? FileUploadRequested; + + private CommandHandler? _commandHandler; public string Draft { - get => _draft; - set => this.RaiseAndSetIfChanged(ref _draft, value); - } + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } = string.Empty; - public string ServerUrl => _serverUrl; + public string ServerUrl { get; private set; } = string.Empty; public ReactiveCommand SendCommand { get; } public bool HasCommandHandler => _commandHandler is not null; - public event Action? SendRequested; - public event Func>? CommandRequested; - public event Action? FileUploadRequested; + public MessageComposerViewModel() + { + IObservable canSend = this.WhenAnyValue(x => x.Draft, draft => !string.IsNullOrWhiteSpace(draft)); + SendCommand = ReactiveCommand.Create(Send, canSend); + } public void RequestFileUpload(string filePath) { - if (!string.IsNullOrEmpty(_serverUrl)) - FileUploadRequested?.Invoke(_serverUrl, filePath); + if (!string.IsNullOrEmpty(ServerUrl)) + { + FileUploadRequested?.Invoke(ServerUrl, filePath); + } } public void SetServer(string serverUrl) { - _serverUrl = serverUrl; + ServerUrl = serverUrl; } public void SetCommandHandler(CommandHandler handler) @@ -53,23 +51,28 @@ public sealed class MessageComposerViewModel : ViewModelBase this.RaisePropertyChanged(nameof(HasCommandHandler)); } - public bool IsCommand(string input) => _commandHandler?.IsCommand(input) ?? input.StartsWith('/'); + public bool IsCommand(string input) + { + return _commandHandler?.IsCommand(input) ?? input.StartsWith('/'); + } private void Send() { - var text = Draft.Trim(); + string text = Draft.Trim(); if (string.IsNullOrWhiteSpace(text)) + { return; + } Draft = string.Empty; if (_commandHandler is not null && _commandHandler.IsCommand(text)) { - CommandRequested?.Invoke(text); + _ = (CommandRequested?.Invoke(text)); } else { - SendRequested?.Invoke(_serverUrl, text); + SendRequested?.Invoke(ServerUrl, text); } } } \ No newline at end of file diff --git a/Decho/ViewModels/MessageViewModel.cs b/Decho/ViewModels/MessageViewModel.cs index 8160d41..df5de02 100644 --- a/Decho/ViewModels/MessageViewModel.cs +++ b/Decho/ViewModels/MessageViewModel.cs @@ -1,18 +1,14 @@ -using System; using Avalonia.Media; -using EchoHub.Core.Models; + using Decho.Models; +using EchoHub.Core.Models; + namespace Decho.ViewModels; -public sealed class MessageViewModel : ViewModelBase +public sealed class MessageViewModel(MessageModel model) : ViewModelBase { - public MessageViewModel(MessageModel model) - { - Model = model; - } - - public MessageModel Model { get; } + public MessageModel Model { get; } = model; public string AuthorName => Model.Author.DisplayName; @@ -20,8 +16,12 @@ public sealed class MessageViewModel : ViewModelBase { get { - var color = Model.Author.NicknameColor; - if (string.IsNullOrEmpty(color)) return null; + string? color = Model.Author.NicknameColor; + if (string.IsNullOrEmpty(color)) + { + return null; + } + try { return new SolidColorBrush(Avalonia.Media.Color.Parse(color)); } catch { return null; } } @@ -51,7 +51,7 @@ public sealed class MessageViewModel : ViewModelBase { get { - var now = DateTimeOffset.Now; + DateTimeOffset now = DateTimeOffset.Now; if (Model.SentAt.Date == now.Date) { diff --git a/Decho/ViewModels/ServerViewModel.cs b/Decho/ViewModels/ServerViewModel.cs index a92157c..23b06ac 100644 --- a/Decho/ViewModels/ServerViewModel.cs +++ b/Decho/ViewModels/ServerViewModel.cs @@ -1,20 +1,117 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; - using Decho.Models; -using ReactiveUI; + +using System.Collections.ObjectModel; +using System.Reactive; namespace Decho.ViewModels; public sealed class ServerViewModel : ViewModelBase { - private bool _isExpanded = true; - private ChannelViewModel? _selectedChannel; + public event Func? ConnectRequested + { + add => _connectRequested = (Func?)Delegate.Combine(_connectRequested, value); + remove => _connectRequested = (Func?)Delegate.Remove(_connectRequested, value); + } + + public event Func? DisconnectRequested + { + add => _disconnectRequested = (Func?)Delegate.Combine(_disconnectRequested, value); + remove => _disconnectRequested = (Func?)Delegate.Remove(_disconnectRequested, value); + } + private bool _isConnected; private bool _isConnecting; private string? _connectedUser; + private Func? _connectRequested; + + private Func? _disconnectRequested; + + public ServerModel Model { get; } + + public string Name => Model.Name; + + public string ServerUrl => Model.ServerUrl; + + public ObservableCollection Channels { get; } + + public ReactiveCommand ConnectCommand { get; } + + public ReactiveCommand DisconnectCommand { get; } + + public ChannelViewModel? SelectedChannel + { + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } + + public bool IsExpanded + { + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } = true; + + public bool IsConnected + { + get => _isConnected; + set + { + _ = this.RaiseAndSetIfChanged(ref _isConnected, value); + this.RaisePropertyChanged(nameof(ConnectionStatusText)); + this.RaisePropertyChanged(nameof(ConnectionStatusColor)); + this.RaisePropertyChanged(nameof(ShowConnectionControls)); + } + } + + public bool IsConnecting + { + get => _isConnecting; + set + { + _ = this.RaiseAndSetIfChanged(ref _isConnecting, value); + this.RaisePropertyChanged(nameof(ConnectionStatusText)); + this.RaisePropertyChanged(nameof(ShowConnectionControls)); + } + } + + public string? ConnectedUser + { + get => _connectedUser; + set => this.RaiseAndSetIfChanged(ref _connectedUser, value); + } + + public string ConnectionStatusText + { + get + { + if (IsConnected) + { + return IsConnecting ? "Connecting..." : $"Connected as {ConnectedUser}"; + } + else + { + return IsConnecting ? "Connecting..." : "Disconnected"; + } + } + } + + public string ConnectionStatusColor + { + get + { + if (IsConnecting) + { + return IsConnected ? "Green" : "Orange"; + } + else + { + return IsConnected ? "Green" : "Gray"; + } + } + } + + public bool ShowConnectionControls => !IsConnected && !IsConnecting; + public ServerViewModel(ServerModel model) { Model = model; @@ -29,91 +126,6 @@ public sealed class ServerViewModel : ViewModelBase DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync); } - public event Func? ConnectRequested - { - add => _connectRequested = (Func?)Delegate.Combine(_connectRequested, value); - remove => _connectRequested = (Func?)Delegate.Remove(_connectRequested, value); - } - private Func? _connectRequested; - - public event Func? DisconnectRequested - { - add => _disconnectRequested = (Func?)Delegate.Combine(_disconnectRequested, value); - remove => _disconnectRequested = (Func?)Delegate.Remove(_disconnectRequested, value); - } - private Func? _disconnectRequested; - - public ServerModel Model { get; } - - public string Name => Model.Name; - - public string ServerUrl => Model.ServerUrl; - - public ObservableCollection Channels { get; } - - public ReactiveCommand ConnectCommand { get; } - public ReactiveCommand DisconnectCommand { get; } - - public ChannelViewModel? SelectedChannel - { - get => _selectedChannel; - set => this.RaiseAndSetIfChanged(ref _selectedChannel, value); - } - - public bool IsExpanded - { - get => _isExpanded; - set => this.RaiseAndSetIfChanged(ref _isExpanded, value); - } - - public bool IsConnected - { - get => _isConnected; - set - { - this.RaiseAndSetIfChanged(ref _isConnected, value); - this.RaisePropertyChanged(nameof(ConnectionStatusText)); - this.RaisePropertyChanged(nameof(ConnectionStatusColor)); - this.RaisePropertyChanged(nameof(ShowConnectionControls)); - } - } - - public bool IsConnecting - { - get => _isConnecting; - set - { - this.RaiseAndSetIfChanged(ref _isConnecting, value); - this.RaisePropertyChanged(nameof(ConnectionStatusText)); - this.RaisePropertyChanged(nameof(ShowConnectionControls)); - } - } - - public string? ConnectedUser - { - get => _connectedUser; - set => this.RaiseAndSetIfChanged(ref _connectedUser, value); - } - - public string ConnectionStatusText => IsConnecting ? "Connecting..." : - IsConnected ? $"Connected as {ConnectedUser}" : "Disconnected"; - - public string ConnectionStatusColor => IsConnected ? "Green" : IsConnecting ? "Orange" : "Gray"; - - public bool ShowConnectionControls => !IsConnected && !IsConnecting; - - private async Task ConnectAsync() - { - if (_connectRequested is not null) - await _connectRequested(); - } - - private async Task DisconnectAsync() - { - if (_disconnectRequested is not null) - await _disconnectRequested(); - } - public void SyncFromModel() { IsConnected = Model.IsConnected; @@ -121,4 +133,20 @@ public sealed class ServerViewModel : ViewModelBase ConnectedUser = Model.ConnectedUser; IsExpanded = true; } + + private async Task ConnectAsync() + { + if (_connectRequested is not null) + { + await _connectRequested(); + } + } + + private async Task DisconnectAsync() + { + if (_disconnectRequested is not null) + { + await _disconnectRequested(); + } + } } \ No newline at end of file diff --git a/Decho/ViewModels/SidebarViewModel.cs b/Decho/ViewModels/SidebarViewModel.cs index 34fc476..0501914 100644 --- a/Decho/ViewModels/SidebarViewModel.cs +++ b/Decho/ViewModels/SidebarViewModel.cs @@ -1,44 +1,37 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Reactive.Linq; -using Decho.Models; -using ReactiveUI; - namespace Decho.ViewModels; public sealed class SidebarViewModel : ViewModelBase { - private ChannelViewModel? _selectedChannel; - - public SidebarViewModel() - { - Servers = new ObservableCollection(); - } - public ObservableCollection Servers { get; } public ChannelViewModel? SelectedChannel { - get => _selectedChannel; + get; set { - if (ReferenceEquals(_selectedChannel, value)) + if (ReferenceEquals(field, value)) + { return; + } - this.RaiseAndSetIfChanged(ref _selectedChannel, value); + _ = this.RaiseAndSetIfChanged(ref field, value); if (value is null) + { return; + } - foreach (var server in Servers) + foreach (ServerViewModel server in Servers) { if (server.Channels.Contains(value)) { if (!ReferenceEquals(server.SelectedChannel, value)) + { server.SelectedChannel = value; + } } else if (server.SelectedChannel is not null) { @@ -48,26 +41,23 @@ public sealed class SidebarViewModel : ViewModelBase } } - public void AddServer(ServerModel model) + public SidebarViewModel() { - var vm = new ServerViewModel(model); - vm.WhenAnyValue(s => s.SelectedChannel) - .Where(channel => channel is not null) - .Subscribe(channel => SelectedChannel = channel!); - - Servers.Add(vm); + Servers = []; } public void RemoveServer(string serverUrl) { - var server = Servers.FirstOrDefault(s => + ServerViewModel? server = Servers.FirstOrDefault(s => string.Equals(s.ServerUrl, serverUrl, StringComparison.OrdinalIgnoreCase)); if (server is not null) { if (SelectedChannel is not null && server.Channels.Contains(SelectedChannel)) + { SelectedChannel = null; + } - Servers.Remove(server); + _ = Servers.Remove(server); } } diff --git a/Decho/ViewModels/ViewModelBase.cs b/Decho/ViewModels/ViewModelBase.cs index 0849b40..7e22c5a 100644 --- a/Decho/ViewModels/ViewModelBase.cs +++ b/Decho/ViewModels/ViewModelBase.cs @@ -1,7 +1,5 @@ -using ReactiveUI; - -namespace Decho.ViewModels; +namespace Decho.ViewModels; public abstract class ViewModelBase : ReactiveObject { -} +} \ No newline at end of file diff --git a/Decho/Views/ChannelListView.axaml.cs b/Decho/Views/ChannelListView.axaml.cs index bad7c1e..c98305b 100644 --- a/Decho/Views/ChannelListView.axaml.cs +++ b/Decho/Views/ChannelListView.axaml.cs @@ -8,4 +8,4 @@ public partial class ChannelListView : UserControl { InitializeComponent(); } -} +} \ No newline at end of file diff --git a/Decho/Views/ChatView.axaml.cs b/Decho/Views/ChatView.axaml.cs index ade7e9a..ccabcbc 100644 --- a/Decho/Views/ChatView.axaml.cs +++ b/Decho/Views/ChatView.axaml.cs @@ -8,4 +8,4 @@ public partial class ChatView : UserControl { InitializeComponent(); } -} +} \ No newline at end of file diff --git a/Decho/Views/MessageComposerView.axaml.cs b/Decho/Views/MessageComposerView.axaml.cs index 28d7321..0c54b45 100644 --- a/Decho/Views/MessageComposerView.axaml.cs +++ b/Decho/Views/MessageComposerView.axaml.cs @@ -1,8 +1,8 @@ -using System.Linq; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform.Storage; + using Decho.ViewModels; namespace Decho.Views; @@ -19,18 +19,24 @@ public partial class MessageComposerView : UserControl private async void OnFileUploadClicked(object? sender, RoutedEventArgs e) { - if (DataContext is not MessageComposerViewModel vm) return; + if (DataContext is not MessageComposerViewModel vm) + { + return; + } - var topLevel = TopLevel.GetTopLevel(this); - if (topLevel?.StorageProvider is null) return; + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel?.StorageProvider is null) + { + return; + } - var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + IReadOnlyList files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { AllowMultiple = false, Title = "Select a file to upload", }); - var file = files?.FirstOrDefault(); + IStorageFile? file = files?.FirstOrDefault(); if (file?.TryGetLocalPath() is string path) { vm.RequestFileUpload(path); @@ -41,21 +47,28 @@ public partial class MessageComposerView : UserControl { #pragma warning disable CS0618 if (e.Data.Contains(DataFormats.Files)) + { #pragma warning restore CS0618 e.DragEffects = DragDropEffects.Copy; + } } private void OnDrop(object? sender, DragEventArgs e) { - if (DataContext is not MessageComposerViewModel vm) return; + if (DataContext is not MessageComposerViewModel vm) + { + return; + } #pragma warning disable CS0618 - var paths = e.Data.GetFiles()? + string? paths = e.Data.GetFiles()? .Select(f => f.TryGetLocalPath()) .FirstOrDefault(p => p is not null); #pragma warning restore CS0618 if (paths is string path) + { vm.RequestFileUpload(path); + } } -} +} \ No newline at end of file diff --git a/Decho/Views/MessageItemView.axaml.cs b/Decho/Views/MessageItemView.axaml.cs index 1b84f65..6af3f96 100644 --- a/Decho/Views/MessageItemView.axaml.cs +++ b/Decho/Views/MessageItemView.axaml.cs @@ -1,10 +1,8 @@ -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; + using Decho.ViewModels; namespace Decho.Views; @@ -30,9 +28,13 @@ public partial class MessageItemView : UserControl private void OnLoaded(object? sender, RoutedEventArgs e) { - if (!_pendingLoad) return; + if (!_pendingLoad) + { + return; + } + _pendingLoad = false; - var msg = (MessageViewModel)DataContext!; + MessageViewModel msg = (MessageViewModel)DataContext!; _ = LoadImageAsync(msg, _loadCts!.Token); } @@ -40,21 +42,26 @@ public partial class MessageItemView : UserControl { try { - var topLevel = TopLevel.GetTopLevel(this); - if (topLevel?.DataContext is not MainWindowViewModel mainVm) return; + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel?.DataContext is not MainWindowViewModel mainVm) + { + return; + } - var bytes = await mainVm.ConnectionService.DownloadImageBytesAsync( + byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync( msg.ServerUrl ?? "", msg.AttachmentUrl!); ct.ThrowIfCancellationRequested(); - if (bytes is null || bytes.Length == 0) return; + if (bytes is null || bytes.Length == 0) + { + return; + } - using var stream = new MemoryStream(bytes); - var bitmap = new Bitmap(stream); + using MemoryStream stream = new MemoryStream(bytes); + Bitmap bitmap = new Bitmap(stream); ct.ThrowIfCancellationRequested(); - var image = this.FindControl("MessageImage"); - if (image is not null) - image.Source = bitmap; + Image? image = this.FindControl("MessageImage"); + _ = image?.Source = bitmap; } catch { @@ -64,21 +71,35 @@ public partial class MessageItemView : UserControl private async void OnDownloadClicked(object? sender, RoutedEventArgs e) { - if (DataContext is not MessageViewModel msg) return; - if (msg.AttachmentUrl is null) return; + if (DataContext is not MessageViewModel msg) + { + return; + } - var topLevel = TopLevel.GetTopLevel(this); - if (topLevel?.StorageProvider is null) return; + if (msg.AttachmentUrl is null) + { + return; + } - var mainVm = topLevel.DataContext as MainWindowViewModel; - if (mainVm is null) return; + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel?.StorageProvider is null) + { + return; + } - var tempPath = await mainVm.ConnectionService.DownloadAttachmentAsync( - msg.ServerUrl ?? "", msg.AttachmentUrl, msg.AttachmentFileName ?? "download"); + if (topLevel.DataContext is not MainWindowViewModel mainVm) + { + return; + } - if (tempPath is null) return; + string? tempPath = await mainVm.ConnectionService.DownloadAttachmentAsync(msg.ServerUrl ?? "", msg.AttachmentUrl, msg.AttachmentFileName ?? "download"); - var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + if (tempPath is null) + { + return; + } + + IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { SuggestedFileName = msg.AttachmentFileName ?? "download", }); @@ -88,7 +109,13 @@ public partial class MessageItemView : UserControl File.Copy(tempPath, savePath, overwrite: true); } - try { File.Delete(tempPath); } - catch { } + try + { + File.Delete(tempPath); + } + catch + { + // Ignore if temp file cannot be deleted + } } -} +} \ No newline at end of file diff --git a/Decho/Views/MessageListView.axaml.cs b/Decho/Views/MessageListView.axaml.cs index bdc97f0..73ff212 100644 --- a/Decho/Views/MessageListView.axaml.cs +++ b/Decho/Views/MessageListView.axaml.cs @@ -1,8 +1,10 @@ -using System.Collections.Specialized; using Avalonia.Controls; using Avalonia.Threading; + using Decho.ViewModels; +using System.Collections.Specialized; + namespace Decho.Views; public partial class MessageListView : UserControl @@ -25,17 +27,15 @@ public partial class MessageListView : UserControl _chat.PropertyChanged -= OnViewModelPropertyChanged; } - var scroll = this.FindControl("MessageScrollViewer"); - if (scroll is not null) - scroll.ScrollChanged -= OnScrollChanged; + ScrollViewer? scroll = this.FindControl("MessageScrollViewer"); + scroll?.ScrollChanged -= OnScrollChanged; _chat = DataContext as ChatViewModel; if (_chat is not null) { _chat.Messages.CollectionChanged += OnMessagesChanged; _chat.PropertyChanged += OnViewModelPropertyChanged; - if (scroll is not null) - scroll.ScrollChanged += OnScrollChanged; + scroll?.ScrollChanged += OnScrollChanged; } } @@ -43,11 +43,11 @@ public partial class MessageListView : UserControl { if (e.ExtentDelta.Y > 0 && _wasAtBottom) { - var scroll = (ScrollViewer)sender!; + ScrollViewer scroll = (ScrollViewer)sender!; Dispatcher.UIThread.Post(scroll.ScrollToEnd, DispatcherPriority.Background); } - var scrollViewer = (ScrollViewer)sender!; + ScrollViewer scrollViewer = (ScrollViewer)sender!; _wasAtBottom = scrollViewer.Offset.Y + scrollViewer.Viewport.Height >= scrollViewer.Extent.Height - 30; } @@ -70,8 +70,11 @@ public partial class MessageListView : UserControl private void TryAutoScroll() { - var scroll = this.FindControl("MessageScrollViewer"); - if (scroll is null) return; + ScrollViewer? scroll = this.FindControl("MessageScrollViewer"); + if (scroll is null) + { + return; + } _wasAtBottom = scroll.Offset.Y + scroll.Viewport.Height >= scroll.Extent.Height - 30; if (_wasAtBottom && !_hasPendingScroll) @@ -87,10 +90,13 @@ public partial class MessageListView : UserControl private void ScrollToBottom() { - var scroll = this.FindControl("MessageScrollViewer"); - if (scroll is null) return; + ScrollViewer? scroll = this.FindControl("MessageScrollViewer"); + if (scroll is null) + { + return; + } _wasAtBottom = true; Dispatcher.UIThread.Post(scroll.ScrollToEnd, DispatcherPriority.Background); } -} +} \ No newline at end of file diff --git a/Decho/Views/SidebarView.axaml.cs b/Decho/Views/SidebarView.axaml.cs index 657486f..d09795d 100644 --- a/Decho/Views/SidebarView.axaml.cs +++ b/Decho/Views/SidebarView.axaml.cs @@ -12,14 +12,14 @@ public partial class SidebarView : UserControl nameof(SelectedChannel), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); - public SidebarView() - { - InitializeComponent(); - } - public ChannelViewModel? SelectedChannel { get => GetValue(SelectedChannelProperty); set => SetValue(SelectedChannelProperty, value); } -} + + public SidebarView() + { + InitializeComponent(); + } +} \ No newline at end of file