From 1a18b372793c9095ac1000c7843288f6eca84763 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:57:19 +0200 Subject: [PATCH] Add password support and UI for joining protected channels --- src/Decho/Models/ChannelModel.cs | 4 +- src/Decho/Services/ConnectionService.cs | 7 +- src/Decho/ViewModels/ChannelViewModel.cs | 8 ++ src/Decho/ViewModels/MainWindowViewModel.cs | 119 +++++++++++++++++--- 4 files changed, 118 insertions(+), 20 deletions(-) diff --git a/src/Decho/Models/ChannelModel.cs b/src/Decho/Models/ChannelModel.cs index b964145..3e37b92 100644 --- a/src/Decho/Models/ChannelModel.cs +++ b/src/Decho/Models/ChannelModel.cs @@ -2,7 +2,7 @@ using System.Collections.ObjectModel; namespace Decho.Models; -public sealed class ChannelModel(string id, string name, ObservableCollection messages, string? topic = null, bool isPublic = true) +public sealed class ChannelModel(string id, string name, ObservableCollection messages, string? topic = null, bool isPublic = true, bool isProtected = false) { public string Id { get; } = id; @@ -12,5 +12,7 @@ public sealed class ChannelModel(string id, string name, ObservableCollection Messages { get; } = messages; } \ No newline at end of file diff --git a/src/Decho/Services/ConnectionService.cs b/src/Decho/Services/ConnectionService.cs index ba65f54..9f00ae9 100644 --- a/src/Decho/Services/ConnectionService.cs +++ b/src/Decho/Services/ConnectionService.cs @@ -157,7 +157,7 @@ public sealed class ConnectionService : IDisposable await entry.Manager.SendMessageAsync(channelName, content); } - public async Task> JoinChannelAsync(string serverUrl, string channelName) + public async Task> JoinChannelAsync(string serverUrl, string channelName, string? password = null) { if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) { @@ -166,7 +166,7 @@ public sealed class ConnectionService : IDisposable if (entry.Manager.TrackChannel(channelName)) { - var outcome = await entry.Manager.JoinChannelAsync(channelName); + var outcome = await entry.Manager.JoinChannelAsync(channelName, password); return outcome.History.Select(m => MessageModelFromDto(m, entry)).ToList(); } @@ -480,7 +480,8 @@ public sealed class ConnectionService : IDisposable dto.Name, [], dto.Topic, - dto.IsPublic); + dto.IsPublic, + dto.IsProtected); } internal static MessageModel MessageModelFromDto(MessageDto dto, ServerConnection entry) diff --git a/src/Decho/ViewModels/ChannelViewModel.cs b/src/Decho/ViewModels/ChannelViewModel.cs index b0735fd..387aca4 100644 --- a/src/Decho/ViewModels/ChannelViewModel.cs +++ b/src/Decho/ViewModels/ChannelViewModel.cs @@ -28,6 +28,14 @@ public sealed class ChannelViewModel(ChannelModel model) : ViewModelBase public bool IsPublic => Model.IsPublic; + public bool IsProtected => Model.IsProtected; + + public bool IsLocked + { + get => field; + set => this.RaiseAndSetIfChanged(ref field, value); + } + public ObservableCollection Messages { get; } = new ObservableCollection( model.Messages.Select(message => new MessageViewModel(message))); diff --git a/src/Decho/ViewModels/MainWindowViewModel.cs b/src/Decho/ViewModels/MainWindowViewModel.cs index ed9ae16..454fcbf 100644 --- a/src/Decho/ViewModels/MainWindowViewModel.cs +++ b/src/Decho/ViewModels/MainWindowViewModel.cs @@ -11,6 +11,7 @@ using EchoHub.Core.DTOs; using EchoHub.Core.Models; using MsBox.Avalonia; +using MsBox.Avalonia.Base; using MsBox.Avalonia.Enums; using System.Reactive; @@ -86,11 +87,11 @@ public sealed class MainWindowViewModel : ViewModelBase } catch (Exception ex) { - var box = MessageBoxManager.GetMessageBoxStandard( + IMsBox box = MessageBoxManager.GetMessageBoxStandard( "Connection Failed", $"Could not connect to server:\n{ex.Message}", ButtonEnum.Ok); - await box.ShowWindowDialogAsync(_mainWindow); + _ = await box.ShowWindowDialogAsync(_mainWindow); } } @@ -136,6 +137,57 @@ public sealed class MainWindowViewModel : ViewModelBase return await dialog.ShowDialog(_mainWindow); } + private async Task ShowPasswordPromptWindowAsync() + { + var window = new Avalonia.Controls.Window + { + Title = "Channel Password", + Width = 320, + Height = 180, + WindowStartupLocation = Avalonia.Controls.WindowStartupLocation.CenterOwner, + SizeToContent = Avalonia.Controls.SizeToContent.Height, + CanResize = false, + }; + + var passwordBox = new Avalonia.Controls.TextBox { Watermark = "Password", PasswordChar = '*' }; + string? result = null; + + var joinBtn = new Avalonia.Controls.Button { Content = "Join", IsDefault = true }; + var cancelBtn = new Avalonia.Controls.Button { Content = "Cancel", IsCancel = true }; + + joinBtn.Click += (_, _) => + { + result = passwordBox.Text; + window.Close(); + }; + cancelBtn.Click += (_, _) => window.Close(); + + var buttons = new Avalonia.Controls.StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + Spacing = 8, + Children = { cancelBtn, joinBtn }, + }; + + var panel = new Avalonia.Controls.StackPanel + { + Margin = new Avalonia.Thickness(12), + Spacing = 8, + Children = + { + new Avalonia.Controls.TextBlock { Text = "This channel is password protected." }, + new Avalonia.Controls.TextBlock { Text = "Enter the channel password:" }, + passwordBox, + buttons, + }, + }; + + window.Content = panel; + await window.ShowDialog(_mainWindow!); + return result; + } + private void WireCommandHandlerEvents() { _commandHandler.OnSetStatus += async (status, message) => @@ -162,7 +214,7 @@ public sealed class MainWindowViewModel : ViewModelBase return; } - List channel = await ConnectionService.JoinChannelAsync(serverUrl, channelName); + List channel = await ConnectionService.JoinChannelAsync(serverUrl, channelName, password); EnsureChannelInList(serverUrl, channelName); ChannelModel? channelModel = FindChannel(serverUrl, channelName); if (channelModel is not null) @@ -456,10 +508,11 @@ public sealed class MainWindowViewModel : ViewModelBase } Sidebar.Servers.Insert(insertIndex, serverVm); - if (serverVm.Channels.Count > 0) - { - serverVm.SelectedChannel = serverVm.Channels[0]; - } + + ChannelViewModel? defaultChannel = serverVm.Channels.FirstOrDefault(c => c.Name == HubConstants.DefaultChannel); + defaultChannel ??= serverVm.Channels.FirstOrDefault(c => c.IsPublic); + defaultChannel ??= serverVm.Channels.FirstOrDefault(); + serverVm.SelectedChannel = defaultChannel; StatusText = $"Connected to {server.Name}"; }); @@ -601,14 +654,26 @@ public sealed class MainWindowViewModel : ViewModelBase { Chat.Composer.SetCommandHandler(new CommandHandler()); + if (channel.IsProtected && channel.Messages.Count == 0) + { + channel.IsLocked = true; + Chat.Composer.IsConnected = false; + } + _ = Task.Run(async () => { - try + string? password = null; + while (true) { - List history = await ConnectionService.JoinChannelAsync(serverUrl, channel.Name); + try + { + List history = await ConnectionService.JoinChannelAsync(serverUrl, channel.Name, password); - Avalonia.Threading.Dispatcher.UIThread.Post(() => + Avalonia.Threading.Dispatcher.UIThread.Post(() => { + channel.IsLocked = false; + Chat.Composer.IsConnected = isServerConnected; + if (channel.Messages.Count == 0) { foreach (MessageModel msg in history) @@ -617,10 +682,32 @@ public sealed class MainWindowViewModel : ViewModelBase } } }); - } - catch - { - // Channel might not be available + + return; + } + catch (EchoHub.Client.Services.ChannelPasswordRequiredException) + { + string? pwd = await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () => + { + if (_mainWindow is null) + { + return null; + } + + return await ShowPasswordPromptWindowAsync(); + }); + + if (string.IsNullOrEmpty(pwd)) + { + return; + } + + password = pwd; + } + catch + { + return; + } } }); } @@ -697,11 +784,11 @@ public sealed class MainWindowViewModel : ViewModelBase catch (Exception ex) { serverVm.IsConnecting = false; - var box = MessageBoxManager.GetMessageBoxStandard( + IMsBox box = MessageBoxManager.GetMessageBoxStandard( "Connection Failed", $"Could not connect to server:\n{ex.Message}", ButtonEnum.Ok); - await box.ShowWindowDialogAsync(_mainWindow); + _ = await box.ShowWindowDialogAsync(_mainWindow); } }