mirror of
https://github.com/Stone-Red-Code/Decho.git
synced 2026-09-06 15:56: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.Constants;
|
||||||
using EchoHub.Core.DTOs;
|
using EchoHub.Core.DTOs;
|
||||||
using EchoHub.Core.Models;
|
using EchoHub.Core.Models;
|
||||||
|
using EchoHub.Core.Security;
|
||||||
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
@@ -192,6 +193,31 @@ public sealed class ConnectionService : IDisposable
|
|||||||
await entry.Manager.SendMessageAsync(channelName, content);
|
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)
|
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))
|
||||||
|
|||||||
@@ -496,6 +496,8 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
serverVm.ConnectRequested += () => HandleServerConnectRequested(serverVm);
|
serverVm.ConnectRequested += () => HandleServerConnectRequested(serverVm);
|
||||||
serverVm.DisconnectRequested += () => HandleServerDisconnectRequested(serverVm);
|
serverVm.DisconnectRequested += () => HandleServerDisconnectRequested(serverVm);
|
||||||
serverVm.RemoveRequested += () => HandleServerRemoveRequested(serverVm);
|
serverVm.RemoveRequested += () => HandleServerRemoveRequested(serverVm);
|
||||||
|
serverVm.CreateChannelRequested += () => HandleCreateChannelRequested(serverVm);
|
||||||
|
serverVm.DeleteChannelRequested += () => HandleDeleteChannelRequested(serverVm);
|
||||||
_ = serverVm.WhenAnyValue(s => s.SelectedChannel)
|
_ = serverVm.WhenAnyValue(s => s.SelectedChannel)
|
||||||
.Where(channel => channel is not null)
|
.Where(channel => channel is not null)
|
||||||
.Subscribe(channel => Sidebar.SelectedChannel = channel!);
|
.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)
|
private void HandleChannelSelected(ChannelViewModel? channel)
|
||||||
{
|
{
|
||||||
if (channel is null)
|
if (channel is null)
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ public sealed class ServerViewModel : ViewModelBase
|
|||||||
remove => _removeRequested = (Func<Task>?)Delegate.Remove(_removeRequested, value);
|
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 _isConnected;
|
||||||
private bool _isConnecting;
|
private bool _isConnecting;
|
||||||
private string? _connectedUser;
|
private string? _connectedUser;
|
||||||
@@ -35,6 +47,10 @@ public sealed class ServerViewModel : ViewModelBase
|
|||||||
|
|
||||||
private Func<Task>? _removeRequested;
|
private Func<Task>? _removeRequested;
|
||||||
|
|
||||||
|
private Func<Task>? _createChannelRequested;
|
||||||
|
|
||||||
|
private Func<Task>? _deleteChannelRequested;
|
||||||
|
|
||||||
public ServerModel Model { get; }
|
public ServerModel Model { get; }
|
||||||
|
|
||||||
public string Name => Model.Name;
|
public string Name => Model.Name;
|
||||||
@@ -49,6 +65,10 @@ public sealed class ServerViewModel : ViewModelBase
|
|||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> RemoveCommand { get; }
|
public ReactiveCommand<Unit, Unit> RemoveCommand { get; }
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> CreateChannelCommand { get; }
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> DeleteChannelCommand { get; }
|
||||||
|
|
||||||
public ChannelViewModel? SelectedChannel
|
public ChannelViewModel? SelectedChannel
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@@ -135,6 +155,8 @@ public sealed class ServerViewModel : ViewModelBase
|
|||||||
ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync);
|
ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync);
|
||||||
DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync);
|
DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync);
|
||||||
RemoveCommand = ReactiveCommand.CreateFromTask(RemoveAsync);
|
RemoveCommand = ReactiveCommand.CreateFromTask(RemoveAsync);
|
||||||
|
CreateChannelCommand = ReactiveCommand.CreateFromTask(CreateChannelAsync);
|
||||||
|
DeleteChannelCommand = ReactiveCommand.CreateFromTask(DeleteChannelAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncFromModel()
|
public void SyncFromModel()
|
||||||
@@ -168,4 +190,20 @@ public sealed class ServerViewModel : ViewModelBase
|
|||||||
await _removeRequested();
|
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}" />
|
IsVisible="{Binding IsConnected}" />
|
||||||
<MenuItem Header="Remove Server"
|
<MenuItem Header="Remove Server"
|
||||||
Command="{Binding RemoveCommand}" />
|
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>
|
</ContextMenu>
|
||||||
</Button.ContextMenu>
|
</Button.ContextMenu>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+1
-1
Submodule src/EchoHub updated: ce41ef9c0a...aae788028e
Reference in New Issue
Block a user