Add password support and UI for joining protected channels

This commit is contained in:
Stone_Red
2026-07-16 13:57:19 +02:00
parent fd72586827
commit 1a18b37279
4 changed files with 118 additions and 20 deletions
+3 -1
View File
@@ -2,7 +2,7 @@ using System.Collections.ObjectModel;
namespace Decho.Models; namespace Decho.Models;
public sealed class ChannelModel(string id, string name, ObservableCollection<MessageModel> messages, string? topic = null, bool isPublic = true) public sealed class ChannelModel(string id, string name, ObservableCollection<MessageModel> messages, string? topic = null, bool isPublic = true, bool isProtected = false)
{ {
public string Id { get; } = id; public string Id { get; } = id;
@@ -12,5 +12,7 @@ public sealed class ChannelModel(string id, string name, ObservableCollection<Me
public bool IsPublic { get; } = isPublic; public bool IsPublic { get; } = isPublic;
public bool IsProtected { get; } = isProtected;
public ObservableCollection<MessageModel> Messages { get; } = messages; public ObservableCollection<MessageModel> Messages { get; } = messages;
} }
+4 -3
View File
@@ -157,7 +157,7 @@ public sealed class ConnectionService : IDisposable
await entry.Manager.SendMessageAsync(channelName, content); await entry.Manager.SendMessageAsync(channelName, content);
} }
public async Task<List<MessageModel>> JoinChannelAsync(string serverUrl, string channelName) public async Task<List<MessageModel>> JoinChannelAsync(string serverUrl, string channelName, string? password = null)
{ {
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
{ {
@@ -166,7 +166,7 @@ public sealed class ConnectionService : IDisposable
if (entry.Manager.TrackChannel(channelName)) 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(); return outcome.History.Select(m => MessageModelFromDto(m, entry)).ToList();
} }
@@ -480,7 +480,8 @@ public sealed class ConnectionService : IDisposable
dto.Name, dto.Name,
[], [],
dto.Topic, dto.Topic,
dto.IsPublic); dto.IsPublic,
dto.IsProtected);
} }
internal static MessageModel MessageModelFromDto(MessageDto dto, ServerConnection entry) internal static MessageModel MessageModelFromDto(MessageDto dto, ServerConnection entry)
+8
View File
@@ -28,6 +28,14 @@ public sealed class ChannelViewModel(ChannelModel model) : ViewModelBase
public bool IsPublic => Model.IsPublic; public bool IsPublic => Model.IsPublic;
public bool IsProtected => Model.IsProtected;
public bool IsLocked
{
get => field;
set => this.RaiseAndSetIfChanged(ref field, value);
}
public ObservableCollection<MessageViewModel> Messages { get; } = new ObservableCollection<MessageViewModel>( public ObservableCollection<MessageViewModel> Messages { get; } = new ObservableCollection<MessageViewModel>(
model.Messages.Select(message => new MessageViewModel(message))); model.Messages.Select(message => new MessageViewModel(message)));
+103 -16
View File
@@ -11,6 +11,7 @@ using EchoHub.Core.DTOs;
using EchoHub.Core.Models; using EchoHub.Core.Models;
using MsBox.Avalonia; using MsBox.Avalonia;
using MsBox.Avalonia.Base;
using MsBox.Avalonia.Enums; using MsBox.Avalonia.Enums;
using System.Reactive; using System.Reactive;
@@ -86,11 +87,11 @@ public sealed class MainWindowViewModel : ViewModelBase
} }
catch (Exception ex) catch (Exception ex)
{ {
var box = MessageBoxManager.GetMessageBoxStandard( IMsBox<ButtonResult> box = MessageBoxManager.GetMessageBoxStandard(
"Connection Failed", "Connection Failed",
$"Could not connect to server:\n{ex.Message}", $"Could not connect to server:\n{ex.Message}",
ButtonEnum.Ok); ButtonEnum.Ok);
await box.ShowWindowDialogAsync(_mainWindow); _ = await box.ShowWindowDialogAsync(_mainWindow);
} }
} }
@@ -136,6 +137,57 @@ public sealed class MainWindowViewModel : ViewModelBase
return await dialog.ShowDialog<ConnectDialogResult?>(_mainWindow); return await dialog.ShowDialog<ConnectDialogResult?>(_mainWindow);
} }
private async Task<string?> 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() private void WireCommandHandlerEvents()
{ {
_commandHandler.OnSetStatus += async (status, message) => _commandHandler.OnSetStatus += async (status, message) =>
@@ -162,7 +214,7 @@ public sealed class MainWindowViewModel : ViewModelBase
return; return;
} }
List<MessageModel> channel = await ConnectionService.JoinChannelAsync(serverUrl, channelName); List<MessageModel> channel = await ConnectionService.JoinChannelAsync(serverUrl, channelName, password);
EnsureChannelInList(serverUrl, channelName); EnsureChannelInList(serverUrl, channelName);
ChannelModel? channelModel = FindChannel(serverUrl, channelName); ChannelModel? channelModel = FindChannel(serverUrl, channelName);
if (channelModel is not null) if (channelModel is not null)
@@ -456,10 +508,11 @@ public sealed class MainWindowViewModel : ViewModelBase
} }
Sidebar.Servers.Insert(insertIndex, serverVm); Sidebar.Servers.Insert(insertIndex, serverVm);
if (serverVm.Channels.Count > 0)
{ ChannelViewModel? defaultChannel = serverVm.Channels.FirstOrDefault(c => c.Name == HubConstants.DefaultChannel);
serverVm.SelectedChannel = serverVm.Channels[0]; defaultChannel ??= serverVm.Channels.FirstOrDefault(c => c.IsPublic);
} defaultChannel ??= serverVm.Channels.FirstOrDefault();
serverVm.SelectedChannel = defaultChannel;
StatusText = $"Connected to {server.Name}"; StatusText = $"Connected to {server.Name}";
}); });
@@ -601,14 +654,26 @@ public sealed class MainWindowViewModel : ViewModelBase
{ {
Chat.Composer.SetCommandHandler(new CommandHandler()); Chat.Composer.SetCommandHandler(new CommandHandler());
if (channel.IsProtected && channel.Messages.Count == 0)
{
channel.IsLocked = true;
Chat.Composer.IsConnected = false;
}
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try string? password = null;
while (true)
{ {
List<MessageModel> history = await ConnectionService.JoinChannelAsync(serverUrl, channel.Name); try
{
List<MessageModel> 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) if (channel.Messages.Count == 0)
{ {
foreach (MessageModel msg in history) foreach (MessageModel msg in history)
@@ -617,10 +682,32 @@ public sealed class MainWindowViewModel : ViewModelBase
} }
} }
}); });
}
catch return;
{ }
// Channel might not be available 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) catch (Exception ex)
{ {
serverVm.IsConnecting = false; serverVm.IsConnecting = false;
var box = MessageBoxManager.GetMessageBoxStandard( IMsBox<ButtonResult> box = MessageBoxManager.GetMessageBoxStandard(
"Connection Failed", "Connection Failed",
$"Could not connect to server:\n{ex.Message}", $"Could not connect to server:\n{ex.Message}",
ButtonEnum.Ok); ButtonEnum.Ok);
await box.ShowWindowDialogAsync(_mainWindow); _ = await box.ShowWindowDialogAsync(_mainWindow);
} }
} }