Add option to remove server and disable input TextBox on channels of disconnected servers

This commit is contained in:
Stone_Red
2026-07-15 23:50:02 +02:00
parent d343eb4043
commit 31a2d874ef
8 changed files with 137 additions and 38 deletions
+30 -1
View File
@@ -137,16 +137,45 @@ public sealed class ConnectionService : IDisposable
entry.Server.IsConnected = false; entry.Server.IsConnected = false;
entry.Server.IsConnecting = false; entry.Server.IsConnecting = false;
ServerStateChanged?.Invoke(entry.Server);
await entry.Manager.CleanupAsync(); await entry.Manager.CleanupAsync();
entry.ApiClient.Dispose(); entry.ApiClient.Dispose();
await entry.Manager.DisposeAsync(); await entry.Manager.DisposeAsync();
_ = _connections.Remove(serverUrl); _ = _connections.Remove(serverUrl);
ServerStateChanged?.Invoke(entry.Server);
}
public async Task RemoveServerAsync(string serverUrl)
{
if (_connections.TryGetValue(serverUrl, out ServerConnection? entry))
{
entry.Server.IsConnected = false;
entry.Server.IsConnecting = false;
await entry.Manager.CleanupAsync();
entry.ApiClient.Dispose();
await entry.Manager.DisposeAsync();
_ = _connections.Remove(serverUrl);
}
RemoveServerFromConfig(serverUrl);
ServerRemoved?.Invoke(serverUrl); ServerRemoved?.Invoke(serverUrl);
} }
private static void RemoveServerFromConfig(string serverUrl)
{
ClientConfig config = ConfigManager.Load();
SavedServer? saved = config.SavedServers.FirstOrDefault(s =>
string.Equals(s.Url, serverUrl, StringComparison.OrdinalIgnoreCase));
if (saved is not null)
{
_ = config.SavedServers.Remove(saved);
ConfigManager.Save(config);
}
}
public async Task SendMessageAsync(string serverUrl, string channelName, string content) public async Task SendMessageAsync(string serverUrl, string channelName, string content)
{ {
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry)) if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
+3 -10
View File
@@ -1,5 +1,3 @@
using EchoHub.Client.Commands;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Decho.ViewModels; namespace Decho.ViewModels;
@@ -45,7 +43,7 @@ public sealed class ChatViewModel : ViewModelBase
Composer = new MessageComposerViewModel(); Composer = new MessageComposerViewModel();
} }
public void SetChannel(ChannelViewModel? channel, string serverUrl = "") public void SetChannel(ChannelViewModel? channel, string serverUrl = "", bool isServerConnected = true)
{ {
if (channel is null) if (channel is null)
{ {
@@ -55,7 +53,7 @@ public sealed class ChatViewModel : ViewModelBase
HasTopic = false; HasTopic = false;
CurrentChannelName = string.Empty; CurrentChannelName = string.Empty;
CurrentServerUrl = string.Empty; CurrentServerUrl = string.Empty;
Composer.SetServer(string.Empty); Composer.SetServer(string.Empty, isServerConnected);
return; return;
} }
@@ -65,12 +63,7 @@ public sealed class ChatViewModel : ViewModelBase
HasTopic = channel.HasTopic; HasTopic = channel.HasTopic;
CurrentChannelName = channel.Name; CurrentChannelName = channel.Name;
CurrentServerUrl = serverUrl; CurrentServerUrl = serverUrl;
Composer.SetServer(serverUrl); Composer.SetServer(serverUrl, isServerConnected);
}
public void SetComposerCommandHandler(CommandHandler handler)
{
Composer.SetCommandHandler(handler);
} }
public void ClearMessages() public void ClearMessages()
+28 -2
View File
@@ -79,6 +79,7 @@ public sealed class MainWindowViewModel : ViewModelBase
ServerViewModel serverVm = new ServerViewModel(placeholderServer); ServerViewModel serverVm = new ServerViewModel(placeholderServer);
serverVm.ConnectRequested += async () => await HandleServerConnectRequested(serverVm); serverVm.ConnectRequested += async () => await HandleServerConnectRequested(serverVm);
serverVm.DisconnectRequested += async () => await HandleServerDisconnectRequested(serverVm); serverVm.DisconnectRequested += async () => await HandleServerDisconnectRequested(serverVm);
serverVm.RemoveRequested += async () => await HandleServerRemoveRequested(serverVm);
Sidebar.Servers.Add(serverVm); Sidebar.Servers.Add(serverVm);
StatusText = "Click Connect on the server to get started"; StatusText = "Click Connect on the server to get started";
@@ -352,6 +353,7 @@ public sealed class MainWindowViewModel : ViewModelBase
ServerViewModel serverVm = new ServerViewModel(server); ServerViewModel serverVm = new ServerViewModel(server);
serverVm.ConnectRequested += () => HandleServerConnectRequested(serverVm); serverVm.ConnectRequested += () => HandleServerConnectRequested(serverVm);
serverVm.DisconnectRequested += () => HandleServerDisconnectRequested(serverVm); serverVm.DisconnectRequested += () => HandleServerDisconnectRequested(serverVm);
serverVm.RemoveRequested += () => HandleServerRemoveRequested(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!);
@@ -365,6 +367,11 @@ public sealed class MainWindowViewModel : ViewModelBase
} }
Sidebar.Servers.Add(serverVm); Sidebar.Servers.Add(serverVm);
if (serverVm.Channels.Count > 0)
{
serverVm.SelectedChannel = serverVm.Channels[0];
}
StatusText = $"Connected to {server.Name}"; StatusText = $"Connected to {server.Name}";
}); });
}; };
@@ -390,6 +397,11 @@ public sealed class MainWindowViewModel : ViewModelBase
if (serverVm is not null) if (serverVm is not null)
{ {
serverVm.SyncFromModel(); serverVm.SyncFromModel();
if (string.Equals(server.ServerUrl, Chat.CurrentServerUrl, StringComparison.OrdinalIgnoreCase))
{
Chat.Composer.IsConnected = server.IsConnected;
}
if (!server.IsConnected) if (!server.IsConnected)
{ {
StatusText = $"Disconnected from {server.Name}"; StatusText = $"Disconnected from {server.Name}";
@@ -493,11 +505,12 @@ public sealed class MainWindowViewModel : ViewModelBase
} }
string serverUrl = FindServerUrlForChannel(channel); string serverUrl = FindServerUrlForChannel(channel);
Chat.SetChannel(channel, serverUrl); bool isServerConnected = Sidebar.GetServer(serverUrl)?.IsConnected ?? false;
Chat.SetChannel(channel, serverUrl, isServerConnected);
if (!string.IsNullOrEmpty(serverUrl)) if (!string.IsNullOrEmpty(serverUrl))
{ {
Chat.SetComposerCommandHandler(new CommandHandler()); Chat.Composer.SetCommandHandler(new CommandHandler());
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
@@ -538,6 +551,7 @@ public sealed class MainWindowViewModel : ViewModelBase
ServerViewModel serverVm = new ServerViewModel(placeholderServer); ServerViewModel serverVm = new ServerViewModel(placeholderServer);
serverVm.ConnectRequested += async () => await HandleServerConnectRequested(serverVm); serverVm.ConnectRequested += async () => await HandleServerConnectRequested(serverVm);
serverVm.DisconnectRequested += async () => await HandleServerDisconnectRequested(serverVm); serverVm.DisconnectRequested += async () => await HandleServerDisconnectRequested(serverVm);
serverVm.RemoveRequested += async () => await HandleServerRemoveRequested(serverVm);
Avalonia.Threading.Dispatcher.UIThread.Post(() => Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{ {
@@ -642,6 +656,18 @@ public sealed class MainWindowViewModel : ViewModelBase
} }
} }
private async Task HandleServerRemoveRequested(ServerViewModel serverVm)
{
try
{
await ConnectionService.RemoveServerAsync(serverVm.ServerUrl);
}
catch (Exception ex)
{
StatusText = $"Remove error: {ex.Message}";
}
}
private void EnsureChannelInList(string serverUrl, string channelName) private void EnsureChannelInList(string serverUrl, string channelName)
{ {
ServerViewModel? serverVm = Sidebar.GetServer(serverUrl); ServerViewModel? serverVm = Sidebar.GetServer(serverUrl);
@@ -22,6 +22,12 @@ public sealed class MessageComposerViewModel : ViewModelBase
public string ServerUrl { get; private set; } = string.Empty; public string ServerUrl { get; private set; } = string.Empty;
public bool IsConnected
{
get;
set => this.RaiseAndSetIfChanged(ref field, value);
} = true;
public ReactiveCommand<Unit, Unit> SendCommand { get; } public ReactiveCommand<Unit, Unit> SendCommand { get; }
public bool HasCommandHandler => _commandHandler is not null; public bool HasCommandHandler => _commandHandler is not null;
@@ -40,9 +46,10 @@ public sealed class MessageComposerViewModel : ViewModelBase
} }
} }
public void SetServer(string serverUrl) public void SetServer(string serverUrl, bool isConnected = true)
{ {
ServerUrl = serverUrl; ServerUrl = serverUrl;
IsConnected = isConnected;
} }
public void SetCommandHandler(CommandHandler handler) public void SetCommandHandler(CommandHandler handler)
+19
View File
@@ -19,6 +19,12 @@ public sealed class ServerViewModel : ViewModelBase
remove => _disconnectRequested = (Func<Task>?)Delegate.Remove(_disconnectRequested, value); remove => _disconnectRequested = (Func<Task>?)Delegate.Remove(_disconnectRequested, value);
} }
public event Func<Task>? RemoveRequested
{
add => _removeRequested = (Func<Task>?)Delegate.Combine(_removeRequested, value);
remove => _removeRequested = (Func<Task>?)Delegate.Remove(_removeRequested, value);
}
private bool _isConnected; private bool _isConnected;
private bool _isConnecting; private bool _isConnecting;
private string? _connectedUser; private string? _connectedUser;
@@ -27,6 +33,8 @@ public sealed class ServerViewModel : ViewModelBase
private Func<Task>? _disconnectRequested; private Func<Task>? _disconnectRequested;
private Func<Task>? _removeRequested;
public ServerModel Model { get; } public ServerModel Model { get; }
public string Name => Model.Name; public string Name => Model.Name;
@@ -39,6 +47,8 @@ public sealed class ServerViewModel : ViewModelBase
public ReactiveCommand<Unit, Unit> DisconnectCommand { get; } public ReactiveCommand<Unit, Unit> DisconnectCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveCommand { get; }
public ChannelViewModel? SelectedChannel public ChannelViewModel? SelectedChannel
{ {
get; get;
@@ -124,6 +134,7 @@ 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);
} }
public void SyncFromModel() public void SyncFromModel()
@@ -149,4 +160,12 @@ public sealed class ServerViewModel : ViewModelBase
await _disconnectRequested(); await _disconnectRequested();
} }
} }
private async Task RemoveAsync()
{
if (_removeRequested is not null)
{
await _removeRequested();
}
}
} }
+6 -3
View File
@@ -8,7 +8,8 @@
<TextBox Grid.Column="0" <TextBox Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="0 0 5 0" Margin="0 0 5 0"
Text="{Binding Draft, Mode=TwoWay}"> Text="{Binding Draft, Mode=TwoWay}"
IsEnabled="{Binding IsConnected}">
<TextBox.KeyBindings> <TextBox.KeyBindings>
<KeyBinding Command="{Binding SendCommand}" Gesture="Enter" /> <KeyBinding Command="{Binding SendCommand}" Gesture="Enter" />
</TextBox.KeyBindings> </TextBox.KeyBindings>
@@ -19,11 +20,13 @@
Margin="0 0 5 0" Margin="0 0 5 0"
Content="Send" Content="Send"
i:Attached.Icon="fa-paper-plane" i:Attached.Icon="fa-paper-plane"
Command="{Binding SendCommand}" /> Command="{Binding SendCommand}"
IsEnabled="{Binding IsConnected}" />
<Button Grid.Column="2" <Button Grid.Column="2"
Height="33" Height="33"
Width="33" Width="33"
Click="OnFileUploadClicked" Click="OnFileUploadClicked"
i:Attached.Icon="fa-paperclip" /> i:Attached.Icon="fa-paperclip"
IsEnabled="{Binding IsConnected}" />
</Grid> </Grid>
</UserControl> </UserControl>
+19 -6
View File
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Decho.ViewModels" xmlns:vm="using:Decho.ViewModels"
xmlns:views="using:Decho.Views" xmlns:views="using:Decho.Views"
xmlns:i="https://github.com/projektanker/icons.avalonia"
x:Class="Decho.Views.SidebarView" x:Class="Decho.Views.SidebarView"
x:DataType="vm:SidebarViewModel"> x:DataType="vm:SidebarViewModel">
<Border Background="{DynamicResource UiTheme00}" Width="250" CornerRadius="0 6 0 0"> <Border Background="{DynamicResource UiTheme00}" Width="250" CornerRadius="0 6 0 0">
@@ -14,6 +15,7 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsExpanded="{Binding IsExpanded}"> IsExpanded="{Binding IsExpanded}">
<Expander.Header> <Expander.Header>
<Grid ColumnDefinitions="*, Auto">
<StackPanel Spacing="5"> <StackPanel Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5"> <StackPanel Orientation="Horizontal" Spacing="5">
<Ellipse Width="8" Height="8" <Ellipse Width="8" Height="8"
@@ -30,6 +32,23 @@
Opacity="0.6" Opacity="0.6"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</StackPanel> </StackPanel>
<Button Grid.Column="1"
x:Name="SettingsButton"
Background="Transparent"
Foreground="{DynamicResource UiTheme08}"
VerticalAlignment="Center"
i:Attached.Icon="fa-cog"
Click="OnSettingsClicked">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Disconnect"
Command="{Binding DisconnectCommand}" />
<MenuItem Header="Remove Server"
Command="{Binding RemoveCommand}" />
</ContextMenu>
</Button.ContextMenu>
</Button>
</Grid>
</Expander.Header> </Expander.Header>
<StackPanel> <StackPanel>
<Button Content="Connect" <Button Content="Connect"
@@ -38,12 +57,6 @@
Margin="4" Margin="4"
Padding="5 2" Padding="5 2"
HorizontalAlignment="Stretch" /> HorizontalAlignment="Stretch" />
<Button Content="Disconnect"
Command="{Binding DisconnectCommand}"
IsVisible="{Binding IsConnected}"
Margin="4"
Padding="5 2"
HorizontalAlignment="Stretch" />
<views:ChannelListView /> <views:ChannelListView />
</StackPanel> </StackPanel>
</Expander> </Expander>
+9
View File
@@ -1,5 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Decho.ViewModels; using Decho.ViewModels;
@@ -22,4 +23,12 @@ public partial class SidebarView : UserControl
{ {
InitializeComponent(); InitializeComponent();
} }
private void OnSettingsClicked(object? sender, RoutedEventArgs e)
{
if (sender is Button button)
{
button.ContextMenu?.Open(button);
}
}
} }