mirror of
https://github.com/Stone-Red-Code/Decho.git
synced 2026-09-04 00:46:11 +02:00
Add channel creation and deletion context menu options
This commit is contained in:
@@ -6,6 +6,7 @@ using EchoHub.Client.UI.Dialogs;
|
||||
using EchoHub.Core.Constants;
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using EchoHub.Core.Security;
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
@@ -192,6 +193,31 @@ public sealed class ConnectionService : IDisposable
|
||||
await entry.Manager.SendMessageAsync(channelName, content);
|
||||
}
|
||||
|
||||
public async Task<ChannelDto?> CreateChannelAsync(string serverUrl, string name, string? topic, bool isPublic, string? password = null)
|
||||
{
|
||||
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||
{
|
||||
throw new InvalidOperationException("Not connected to server");
|
||||
}
|
||||
|
||||
string? wirePassword = null, saltB64 = null, wrappedKey = null;
|
||||
|
||||
if (password is not null)
|
||||
{
|
||||
var salt = RoomCrypto.GenerateSalt();
|
||||
var derived = RoomCrypto.DeriveKeys(password, salt);
|
||||
var roomKey = RoomCrypto.GenerateRoomKey();
|
||||
wirePassword = derived.AuthKeyHex;
|
||||
saltB64 = Convert.ToBase64String(salt);
|
||||
wrappedKey = RoomCrypto.WrapRoomKey(roomKey, derived.KeyEncryptionKey);
|
||||
// Store the room key locally so we can decrypt messages immediately
|
||||
entry.Manager.RoomKeys.StoreKey(name, roomKey);
|
||||
}
|
||||
|
||||
ChannelDto? channel = await entry.ApiClient.CreateChannelAsync(name, topic, isPublic, wirePassword, saltB64, wrappedKey);
|
||||
return channel;
|
||||
}
|
||||
|
||||
public async Task<List<MessageModel>> JoinChannelAsync(string serverUrl, string channelName, string? password = null)
|
||||
{
|
||||
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||
|
||||
@@ -496,6 +496,8 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
serverVm.ConnectRequested += () => HandleServerConnectRequested(serverVm);
|
||||
serverVm.DisconnectRequested += () => HandleServerDisconnectRequested(serverVm);
|
||||
serverVm.RemoveRequested += () => HandleServerRemoveRequested(serverVm);
|
||||
serverVm.CreateChannelRequested += () => HandleCreateChannelRequested(serverVm);
|
||||
serverVm.DeleteChannelRequested += () => HandleDeleteChannelRequested(serverVm);
|
||||
_ = serverVm.WhenAnyValue(s => s.SelectedChannel)
|
||||
.Where(channel => channel is not null)
|
||||
.Subscribe(channel => Sidebar.SelectedChannel = channel!);
|
||||
@@ -659,6 +661,112 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleCreateChannelRequested(ServerViewModel server)
|
||||
{
|
||||
if (_mainWindow is null) return;
|
||||
|
||||
CreateChannelWindow dialog = new CreateChannelWindow();
|
||||
bool? result = await dialog.ShowDialog<bool?>(_mainWindow);
|
||||
if (result != true) return;
|
||||
|
||||
try
|
||||
{
|
||||
StatusText = "Creating channel...";
|
||||
|
||||
ChannelDto? channel = await ConnectionService.CreateChannelAsync(
|
||||
server.ServerUrl, dialog.ResultName!, dialog.ResultTopic, dialog.ResultIsPublic, dialog.ResultPassword);
|
||||
|
||||
if (channel is null)
|
||||
{
|
||||
StatusText = "Failed to create channel";
|
||||
return;
|
||||
}
|
||||
|
||||
List<MessageModel> history = await ConnectionService.JoinChannelAsync(server.ServerUrl, channel.Name);
|
||||
|
||||
ChannelModel channelModel = new ChannelModel(
|
||||
channel.Id.ToString(), channel.Name, [], channel.Topic, channel.IsPublic, channel.IsProtected);
|
||||
|
||||
ServerViewModel? serverVm = Sidebar.GetServer(server.ServerUrl);
|
||||
if (serverVm is not null)
|
||||
{
|
||||
ChannelViewModel channelVm = new ChannelViewModel(channelModel);
|
||||
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
serverVm.Channels.Add(channelVm);
|
||||
serverVm.SelectedChannel = channelVm;
|
||||
|
||||
foreach (MessageModel msg in history)
|
||||
{
|
||||
channelVm.AddMessage(msg);
|
||||
}
|
||||
|
||||
StatusText = $"Created #{channel.Name}";
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IMsBox<ButtonResult> box = MessageBoxManager.GetMessageBoxStandard(
|
||||
"Error", $"Could not create channel:\n{ex.Message}", ButtonEnum.Ok);
|
||||
_ = await box.ShowWindowDialogAsync(_mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleDeleteChannelRequested(ServerViewModel server)
|
||||
{
|
||||
if (_mainWindow is null) return;
|
||||
|
||||
string channelName = Chat.CurrentChannelName;
|
||||
if (string.IsNullOrEmpty(channelName) || !string.Equals(Chat.CurrentServerUrl, server.ServerUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
StatusText = "No channel selected on this server";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(channelName, HubConstants.DefaultChannel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
IMsBox<ButtonResult> box = MessageBoxManager.GetMessageBoxStandard(
|
||||
"Cannot Delete", $"The #{HubConstants.DefaultChannel} channel cannot be deleted.", ButtonEnum.Ok);
|
||||
_ = await box.ShowWindowDialogAsync(_mainWindow);
|
||||
return;
|
||||
}
|
||||
|
||||
IMsBox<ButtonResult> confirmBox = MessageBoxManager.GetMessageBoxStandard(
|
||||
"Delete Channel",
|
||||
$"Are you sure you want to delete #{channelName}?\nThis will remove all messages permanently.",
|
||||
ButtonEnum.YesNo);
|
||||
ButtonResult confirm = await confirmBox.ShowWindowDialogAsync(_mainWindow);
|
||||
if (confirm != ButtonResult.Yes) return;
|
||||
|
||||
try
|
||||
{
|
||||
await ConnectionService.DeleteChannelAsync(server.ServerUrl, channelName);
|
||||
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ChannelViewModel? channelVm = server.Channels.FirstOrDefault(c => c.Name == channelName);
|
||||
if (channelVm is not null)
|
||||
{
|
||||
_ = server.Channels.Remove(channelVm);
|
||||
}
|
||||
|
||||
ChannelViewModel? defaultChannel = server.Channels.FirstOrDefault(c => c.Name == HubConstants.DefaultChannel)
|
||||
?? server.Channels.FirstOrDefault();
|
||||
server.SelectedChannel = defaultChannel;
|
||||
|
||||
StatusText = $"Deleted #{channelName}";
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IMsBox<ButtonResult> box = MessageBoxManager.GetMessageBoxStandard(
|
||||
"Error", $"Could not delete channel:\n{ex.Message}", ButtonEnum.Ok);
|
||||
_ = await box.ShowWindowDialogAsync(_mainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleChannelSelected(ChannelViewModel? channel)
|
||||
{
|
||||
if (channel is null)
|
||||
|
||||
@@ -25,6 +25,18 @@ public sealed class ServerViewModel : ViewModelBase
|
||||
remove => _removeRequested = (Func<Task>?)Delegate.Remove(_removeRequested, value);
|
||||
}
|
||||
|
||||
public event Func<Task>? CreateChannelRequested
|
||||
{
|
||||
add => _createChannelRequested = (Func<Task>?)Delegate.Combine(_createChannelRequested, value);
|
||||
remove => _createChannelRequested = (Func<Task>?)Delegate.Remove(_createChannelRequested, value);
|
||||
}
|
||||
|
||||
public event Func<Task>? DeleteChannelRequested
|
||||
{
|
||||
add => _deleteChannelRequested = (Func<Task>?)Delegate.Combine(_deleteChannelRequested, value);
|
||||
remove => _deleteChannelRequested = (Func<Task>?)Delegate.Remove(_deleteChannelRequested, value);
|
||||
}
|
||||
|
||||
private bool _isConnected;
|
||||
private bool _isConnecting;
|
||||
private string? _connectedUser;
|
||||
@@ -35,6 +47,10 @@ public sealed class ServerViewModel : ViewModelBase
|
||||
|
||||
private Func<Task>? _removeRequested;
|
||||
|
||||
private Func<Task>? _createChannelRequested;
|
||||
|
||||
private Func<Task>? _deleteChannelRequested;
|
||||
|
||||
public ServerModel Model { get; }
|
||||
|
||||
public string Name => Model.Name;
|
||||
@@ -49,6 +65,10 @@ public sealed class ServerViewModel : ViewModelBase
|
||||
|
||||
public ReactiveCommand<Unit, Unit> RemoveCommand { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> CreateChannelCommand { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> DeleteChannelCommand { get; }
|
||||
|
||||
public ChannelViewModel? SelectedChannel
|
||||
{
|
||||
get;
|
||||
@@ -135,6 +155,8 @@ public sealed class ServerViewModel : ViewModelBase
|
||||
ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync);
|
||||
DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync);
|
||||
RemoveCommand = ReactiveCommand.CreateFromTask(RemoveAsync);
|
||||
CreateChannelCommand = ReactiveCommand.CreateFromTask(CreateChannelAsync);
|
||||
DeleteChannelCommand = ReactiveCommand.CreateFromTask(DeleteChannelAsync);
|
||||
}
|
||||
|
||||
public void SyncFromModel()
|
||||
@@ -168,4 +190,20 @@ public sealed class ServerViewModel : ViewModelBase
|
||||
await _removeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateChannelAsync()
|
||||
{
|
||||
if (_createChannelRequested is not null)
|
||||
{
|
||||
await _createChannelRequested();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteChannelAsync()
|
||||
{
|
||||
if (_deleteChannelRequested is not null)
|
||||
{
|
||||
await _deleteChannelRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Decho.Views.CreateChannelWindow"
|
||||
Title="Create Channel"
|
||||
Width="380"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False">
|
||||
<StackPanel Margin="12" Spacing="8">
|
||||
<TextBox Name="ChannelName" Watermark="Channel name" />
|
||||
<TextBox Name="ChannelTopic" Watermark="Topic (optional)" />
|
||||
<CheckBox Name="PublicCheckBox" Content="Public" IsChecked="True" />
|
||||
<TextBox Name="ChannelPassword" Watermark="Password (optional for private channels)" PasswordChar="*" />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Content="Cancel" IsCancel="True" />
|
||||
<Button Content="Create" IsDefault="True" Click="OnCreateClicked" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,32 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Decho.Views;
|
||||
|
||||
public partial class CreateChannelWindow : Window
|
||||
{
|
||||
public string? ResultName { get; private set; }
|
||||
public string? ResultTopic { get; private set; }
|
||||
public bool ResultIsPublic { get; private set; } = true;
|
||||
public string? ResultPassword { get; private set; }
|
||||
|
||||
public CreateChannelWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnCreateClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
string name = ChannelName.Text?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
ChannelName.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
ResultName = name;
|
||||
ResultTopic = string.IsNullOrWhiteSpace(ChannelTopic.Text) ? null : ChannelTopic.Text.Trim();
|
||||
ResultIsPublic = PublicCheckBox.IsChecked == true;
|
||||
ResultPassword = string.IsNullOrWhiteSpace(ChannelPassword.Text) ? null : ChannelPassword.Text;
|
||||
Close(true);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,13 @@
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
<MenuItem Header="Remove Server"
|
||||
Command="{Binding RemoveCommand}" />
|
||||
<Separator />
|
||||
<MenuItem Header="Create Channel"
|
||||
Command="{Binding CreateChannelCommand}"
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
<MenuItem Header="Delete Channel"
|
||||
Command="{Binding DeleteChannelCommand}"
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
|
||||
+1
-1
Submodule src/EchoHub updated: ce41ef9c0a...aae788028e
Reference in New Issue
Block a user