mirror of
https://github.com/Stone-Red-Code/Decho.git
synced 2026-09-04 08:56:14 +02:00
Code cleanup
This commit is contained in:
@@ -2,29 +2,15 @@ using System.Collections.ObjectModel;
|
|||||||
|
|
||||||
namespace Decho.Models;
|
namespace Decho.Models;
|
||||||
|
|
||||||
public sealed class ChannelModel
|
public sealed class ChannelModel(string id, string name, ObservableCollection<MessageModel> messages, string? topic = null, bool isPublic = true)
|
||||||
{
|
{
|
||||||
public ChannelModel(
|
public string Id { get; } = id;
|
||||||
string id,
|
|
||||||
string name,
|
|
||||||
ObservableCollection<MessageModel> messages,
|
|
||||||
string? topic = null,
|
|
||||||
bool isPublic = true)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Name = name;
|
|
||||||
Messages = messages;
|
|
||||||
Topic = topic;
|
|
||||||
IsPublic = isPublic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
public string Name { get; } = name;
|
||||||
|
|
||||||
public string Name { get; }
|
public string? Topic { get; set; } = topic;
|
||||||
|
|
||||||
public string? Topic { get; set; }
|
public bool IsPublic { get; } = isPublic;
|
||||||
|
|
||||||
public bool IsPublic { get; }
|
public ObservableCollection<MessageModel> Messages { get; } = messages;
|
||||||
|
}
|
||||||
public ObservableCollection<MessageModel> Messages { get; }
|
|
||||||
}
|
|
||||||
@@ -2,49 +2,25 @@ using EchoHub.Core.Models;
|
|||||||
|
|
||||||
namespace Decho.Models;
|
namespace Decho.Models;
|
||||||
|
|
||||||
public sealed class MessageModel
|
public sealed class MessageModel(string id, UserModel author, DateTimeOffset sentAt, string content, string channelName, string? serverUrl = null, MessageType type = MessageType.Text, string? attachmentUrl = null, string? attachmentFileName = null, long? attachmentFileSize = null)
|
||||||
{
|
{
|
||||||
public MessageModel(
|
public string Id { get; } = id;
|
||||||
string id,
|
|
||||||
UserModel author,
|
|
||||||
DateTimeOffset sentAt,
|
|
||||||
string content,
|
|
||||||
string channelName,
|
|
||||||
string? serverUrl = null,
|
|
||||||
MessageType type = MessageType.Text,
|
|
||||||
string? attachmentUrl = null,
|
|
||||||
string? attachmentFileName = null,
|
|
||||||
long? attachmentFileSize = null)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Author = author;
|
|
||||||
SentAt = sentAt;
|
|
||||||
Content = content;
|
|
||||||
ChannelName = channelName;
|
|
||||||
ServerUrl = serverUrl;
|
|
||||||
Type = type;
|
|
||||||
AttachmentUrl = attachmentUrl;
|
|
||||||
AttachmentFileName = attachmentFileName;
|
|
||||||
AttachmentFileSize = attachmentFileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
public UserModel Author { get; } = author;
|
||||||
|
|
||||||
public UserModel Author { get; }
|
public DateTimeOffset SentAt { get; } = sentAt;
|
||||||
|
|
||||||
public DateTimeOffset SentAt { get; }
|
public string Content { get; } = content;
|
||||||
|
|
||||||
public string Content { get; }
|
public string ChannelName { get; } = channelName;
|
||||||
|
|
||||||
public string ChannelName { get; }
|
public string? ServerUrl { get; } = serverUrl;
|
||||||
|
|
||||||
public string? ServerUrl { get; }
|
public MessageType Type { get; } = type;
|
||||||
|
|
||||||
public MessageType Type { get; }
|
public string? AttachmentUrl { get; } = attachmentUrl;
|
||||||
|
|
||||||
public string? AttachmentUrl { get; }
|
public string? AttachmentFileName { get; } = attachmentFileName;
|
||||||
|
|
||||||
public string? AttachmentFileName { get; }
|
public long? AttachmentFileSize { get; } = attachmentFileSize;
|
||||||
|
}
|
||||||
public long? AttachmentFileSize { get; }
|
|
||||||
}
|
|
||||||
@@ -2,37 +2,19 @@ using System.Collections.ObjectModel;
|
|||||||
|
|
||||||
namespace Decho.Models;
|
namespace Decho.Models;
|
||||||
|
|
||||||
public sealed class ServerModel
|
public sealed class ServerModel(string id, string name, ObservableCollection<ChannelModel> channels, string serverUrl = "", bool isConnected = false, bool isConnecting = false, string? connectedUser = null)
|
||||||
{
|
{
|
||||||
public ServerModel(
|
public string Id { get; } = id;
|
||||||
string id,
|
|
||||||
string name,
|
|
||||||
ObservableCollection<ChannelModel> channels,
|
|
||||||
string serverUrl = "",
|
|
||||||
bool isConnected = false,
|
|
||||||
bool isConnecting = false,
|
|
||||||
string? connectedUser = null)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Name = name;
|
|
||||||
Channels = channels;
|
|
||||||
ServerUrl = serverUrl;
|
|
||||||
IsConnected = isConnected;
|
|
||||||
IsConnecting = isConnecting;
|
|
||||||
ConnectedUser = connectedUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
public string Name { get; } = name;
|
||||||
|
|
||||||
public string Name { get; }
|
public string ServerUrl { get; set; } = serverUrl;
|
||||||
|
|
||||||
public string ServerUrl { get; set; }
|
public bool IsConnected { get; set; } = isConnected;
|
||||||
|
|
||||||
public bool IsConnected { get; set; }
|
public bool IsConnecting { get; set; } = isConnecting;
|
||||||
|
|
||||||
public bool IsConnecting { get; set; }
|
public string? ConnectedUser { get; set; } = connectedUser;
|
||||||
|
|
||||||
public string? ConnectedUser { get; set; }
|
public ObservableCollection<ChannelModel> Channels { get; } = channels;
|
||||||
|
}
|
||||||
public ObservableCollection<ChannelModel> Channels { get; }
|
|
||||||
}
|
|
||||||
@@ -2,29 +2,15 @@ using EchoHub.Core.Models;
|
|||||||
|
|
||||||
namespace Decho.Models;
|
namespace Decho.Models;
|
||||||
|
|
||||||
public sealed class UserModel
|
public sealed class UserModel(string id, string displayName, string? nicknameColor = null, UserStatus status = UserStatus.Online, string? statusMessage = null)
|
||||||
{
|
{
|
||||||
public UserModel(
|
public string Id { get; } = id;
|
||||||
string id,
|
|
||||||
string displayName,
|
|
||||||
string? nicknameColor = null,
|
|
||||||
UserStatus status = UserStatus.Online,
|
|
||||||
string? statusMessage = null)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
DisplayName = displayName;
|
|
||||||
NicknameColor = nicknameColor;
|
|
||||||
Status = status;
|
|
||||||
StatusMessage = statusMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
public string DisplayName { get; } = displayName;
|
||||||
|
|
||||||
public string DisplayName { get; }
|
public string? NicknameColor { get; set; } = nicknameColor;
|
||||||
|
|
||||||
public string? NicknameColor { get; set; }
|
public UserStatus Status { get; set; } = status;
|
||||||
|
|
||||||
public UserStatus Status { get; set; }
|
public string? StatusMessage { get; set; } = statusMessage;
|
||||||
|
}
|
||||||
public string? StatusMessage { get; set; }
|
|
||||||
}
|
|
||||||
+7
-6
@@ -6,23 +6,24 @@ using Avalonia;
|
|||||||
using Projektanker.Icons.Avalonia;
|
using Projektanker.Icons.Avalonia;
|
||||||
using Projektanker.Icons.Avalonia.FontAwesome;
|
using Projektanker.Icons.Avalonia.FontAwesome;
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Decho;
|
namespace Decho;
|
||||||
|
|
||||||
internal sealed class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
// yet and stuff might break.
|
// yet and stuff might break.
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
_ = BuildAvaloniaApp()
|
||||||
.StartWithClassicDesktopLifetime(args);
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
{
|
{
|
||||||
IconProvider.Current
|
_ = IconProvider.Current
|
||||||
.Register<FontAwesomeIconProvider>();
|
.Register<FontAwesomeIconProvider>();
|
||||||
|
|
||||||
return AppBuilder.Configure<App>()
|
return AppBuilder.Configure<App>()
|
||||||
@@ -31,4 +32,4 @@ internal sealed class Program
|
|||||||
.LogToTrace()
|
.LogToTrace()
|
||||||
.UseReactiveUI();
|
.UseReactiveUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+423
-338
@@ -1,38 +1,45 @@
|
|||||||
using System.Collections.ObjectModel;
|
using Decho.Models;
|
||||||
|
|
||||||
|
using EchoHub.Client.Commands;
|
||||||
using EchoHub.Client.Config;
|
using EchoHub.Client.Config;
|
||||||
using EchoHub.Client.Services;
|
using EchoHub.Client.Services;
|
||||||
using EchoHub.Client.Commands;
|
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 Decho.Models;
|
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Decho.Services;
|
namespace Decho.Services;
|
||||||
|
|
||||||
public sealed class ConnectionService : IDisposable
|
public sealed class ConnectionService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, ServerConnection> _connections = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
public event Action<ServerModel>? ServerAdded;
|
public event Action<ServerModel>? ServerAdded;
|
||||||
|
|
||||||
public event Action<string>? ServerRemoved;
|
public event Action<string>? ServerRemoved;
|
||||||
|
|
||||||
public event Action<ServerModel>? ServerStateChanged;
|
public event Action<ServerModel>? ServerStateChanged;
|
||||||
|
|
||||||
public event Action<string, ChannelModel>? ChannelAdded;
|
public event Action<string, ChannelModel>? ChannelAdded;
|
||||||
|
|
||||||
public event Action<string, string>? ChannelRemoved;
|
public event Action<string, string>? ChannelRemoved;
|
||||||
|
|
||||||
public event Action<string, MessageModel>? MessageReceived;
|
public event Action<string, MessageModel>? MessageReceived;
|
||||||
|
|
||||||
public event Action<string, string, string?>? UserJoined;
|
public event Action<string, string, string?>? UserJoined;
|
||||||
|
|
||||||
public event Action<string, string>? UserLeft;
|
public event Action<string, string>? UserLeft;
|
||||||
|
|
||||||
public event Action<string, string>? ErrorOccurred;
|
public event Action<string, string>? ErrorOccurred;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, ServerConnection> _connections = new(StringComparer.OrdinalIgnoreCase);
|
||||||
internal IReadOnlyDictionary<string, ServerConnection> Connections => _connections;
|
internal IReadOnlyDictionary<string, ServerConnection> Connections => _connections;
|
||||||
|
|
||||||
internal ServerConnection? GetConnection(string serverUrl) =>
|
|
||||||
_connections.TryGetValue(serverUrl, out var conn) ? conn : null;
|
|
||||||
|
|
||||||
public async Task<ServerModel> ConnectAsync(string serverUrl, string username, string password, bool isRegister, bool rememberMe)
|
public async Task<ServerModel> ConnectAsync(string serverUrl, string username, string password, bool isRegister, bool rememberMe)
|
||||||
{
|
{
|
||||||
var conn = new ConnectionManager();
|
ConnectionManager conn = new ConnectionManager();
|
||||||
|
|
||||||
var dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult(
|
ConnectDialogResult dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult(
|
||||||
serverUrl, username, password, isRegister, rememberMe, null);
|
serverUrl, username, password, isRegister, rememberMe, null);
|
||||||
|
|
||||||
ConnectResult result;
|
ConnectResult result;
|
||||||
@@ -46,12 +53,12 @@ public sealed class ConnectionService : IDisposable
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
var login = result.Login;
|
LoginResponse login = result.Login;
|
||||||
var userModel = new UserModel(login.Username, login.DisplayName ?? login.Username,
|
UserModel userModel = new UserModel(login.Username, login.DisplayName ?? login.Username,
|
||||||
login.NicknameColor);
|
login.NicknameColor);
|
||||||
|
|
||||||
var channels = new ObservableCollection<ChannelModel>();
|
ObservableCollection<ChannelModel> channels = [];
|
||||||
var serverModel = new ServerModel(
|
ServerModel serverModel = new ServerModel(
|
||||||
Guid.NewGuid().ToString("N"),
|
Guid.NewGuid().ToString("N"),
|
||||||
new Uri(serverUrl).Host,
|
new Uri(serverUrl).Host,
|
||||||
channels,
|
channels,
|
||||||
@@ -59,11 +66,11 @@ public sealed class ConnectionService : IDisposable
|
|||||||
isConnected: true,
|
isConnected: true,
|
||||||
connectedUser: login.Username);
|
connectedUser: login.Username);
|
||||||
|
|
||||||
var serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel);
|
ServerConnection serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel);
|
||||||
|
|
||||||
foreach (var ch in result.Channels)
|
foreach (ChannelDto ch in result.Channels)
|
||||||
{
|
{
|
||||||
var channelModel = ChannelModelFromDto(ch);
|
ChannelModel channelModel = ChannelModelFromDto(ch);
|
||||||
channels.Add(channelModel);
|
channels.Add(channelModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,9 +85,9 @@ public sealed class ConnectionService : IDisposable
|
|||||||
|
|
||||||
public async Task ConnectWithSavedTokenAsync(string serverUrl, string username, string refreshToken, bool rememberMe)
|
public async Task ConnectWithSavedTokenAsync(string serverUrl, string username, string refreshToken, bool rememberMe)
|
||||||
{
|
{
|
||||||
var conn = new ConnectionManager();
|
ConnectionManager conn = new ConnectionManager();
|
||||||
|
|
||||||
var dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult(
|
ConnectDialogResult dialogResult = new EchoHub.Client.UI.Dialogs.ConnectDialogResult(
|
||||||
serverUrl, username, "", false, rememberMe, refreshToken);
|
serverUrl, username, "", false, rememberMe, refreshToken);
|
||||||
|
|
||||||
ConnectResult result;
|
ConnectResult result;
|
||||||
@@ -94,11 +101,11 @@ public sealed class ConnectionService : IDisposable
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
var login = result.Login;
|
LoginResponse login = result.Login;
|
||||||
var userModel = new UserModel(login.Username, login.DisplayName ?? login.Username, login.NicknameColor);
|
UserModel userModel = new UserModel(login.Username, login.DisplayName ?? login.Username, login.NicknameColor);
|
||||||
|
|
||||||
var channels = new ObservableCollection<ChannelModel>();
|
ObservableCollection<ChannelModel> channels = [];
|
||||||
var serverModel = new ServerModel(
|
ServerModel serverModel = new ServerModel(
|
||||||
Guid.NewGuid().ToString("N"),
|
Guid.NewGuid().ToString("N"),
|
||||||
new Uri(serverUrl).Host,
|
new Uri(serverUrl).Host,
|
||||||
channels,
|
channels,
|
||||||
@@ -106,11 +113,11 @@ public sealed class ConnectionService : IDisposable
|
|||||||
isConnected: true,
|
isConnected: true,
|
||||||
connectedUser: login.Username);
|
connectedUser: login.Username);
|
||||||
|
|
||||||
var serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel);
|
ServerConnection serverEntry = new ServerConnection(conn, conn.Api!, serverModel, userModel);
|
||||||
|
|
||||||
foreach (var ch in result.Channels)
|
foreach (ChannelDto ch in result.Channels)
|
||||||
{
|
{
|
||||||
var channelModel = ChannelModelFromDto(ch);
|
ChannelModel channelModel = ChannelModelFromDto(ch);
|
||||||
channels.Add(channelModel);
|
channels.Add(channelModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,15 +128,395 @@ public sealed class ConnectionService : IDisposable
|
|||||||
ServerAdded?.Invoke(serverModel);
|
ServerAdded?.Invoke(serverModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DisconnectAsync(string serverUrl)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Server.IsConnected = false;
|
||||||
|
entry.Server.IsConnecting = false;
|
||||||
|
ServerStateChanged?.Invoke(entry.Server);
|
||||||
|
|
||||||
|
await entry.Manager.CleanupAsync();
|
||||||
|
entry.ApiClient.Dispose();
|
||||||
|
await entry.Manager.DisposeAsync();
|
||||||
|
|
||||||
|
_ = _connections.Remove(serverUrl);
|
||||||
|
ServerRemoved?.Invoke(serverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessageAsync(string serverUrl, string channelName, string content)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Not connected to server");
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.Manager.SendMessageAsync(channelName, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MessageModel>> JoinChannelAsync(string serverUrl, string channelName)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Not connected to server");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Manager.TrackChannel(channelName))
|
||||||
|
{
|
||||||
|
List<MessageDto> history = await entry.Manager.JoinChannelAsync(channelName);
|
||||||
|
return history.Select(m => MessageModelFromDto(m, entry)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MessageDto> existing = await entry.Manager.GetHistoryAsync(channelName);
|
||||||
|
return existing.Select(m => MessageModelFromDto(m, entry)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LeaveChannelAsync(string serverUrl, string channelName)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.Manager.LeaveChannelAsync(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MessageModel>> GetHistoryAsync(string serverUrl, string channelName, int count = HubConstants.DefaultHistoryCount, int offset = 0)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MessageDto> history = await entry.Manager.GetHistoryAsync(channelName, count, offset);
|
||||||
|
return history.Select(m => MessageModelFromDto(m, entry)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<UserPresenceDto>> GetOnlineUsersAsync(string serverUrl, string channelName)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await entry.Manager.GetOnlineUsersAsync(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatusAsync(string serverUrl, UserStatus status, string? statusMessage)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.Manager.UpdateStatusAsync(status, statusMessage);
|
||||||
|
entry.User.Status = status;
|
||||||
|
entry.User.StatusMessage = statusMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChannelDto?> CreateChannelAsync(string serverUrl, string name, string? topic, bool isPublic)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await entry.ApiClient.CreateChannelAsync(name, topic, isPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteChannelAsync(string serverUrl, string channelName)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.DeleteChannelAsync(channelName);
|
||||||
|
entry.Manager.UntrackChannel(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task KickUserAsync(string serverUrl, string username, string? reason)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.KickUserAsync(username, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BanUserAsync(string serverUrl, string username, string? reason)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.BanUserAsync(username, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnbanUserAsync(string serverUrl, string username)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.UnbanUserAsync(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MuteUserAsync(string serverUrl, string username, int? durationMinutes)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.MuteUserAsync(username, durationMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnmuteUserAsync(string serverUrl, string username)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.UnmuteUserAsync(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AssignRoleAsync(string serverUrl, string username, ServerRole role)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.AssignRoleAsync(username, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NukeChannelAsync(string serverUrl, string channelName)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await entry.ApiClient.NukeChannelAsync(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateProfileAsync(string serverUrl, string? displayName, string? bio, string? nickColor)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = await entry.ApiClient.UpdateProfileAsync(new UpdateProfileRequest(displayName, bio, nickColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetAvatarAsync(string serverUrl, string target)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = await AvatarHelper.UploadAsync(entry.ApiClient, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendUrlAsync(string serverUrl, string channelName, string url, string? size)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = await entry.ApiClient.SendUrlAsync(channelName, url, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UploadFileAsync(string serverUrl, string channelName, string filePath, string? size)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using FileStream stream = File.OpenRead(filePath);
|
||||||
|
string fileName = Path.GetFileName(filePath);
|
||||||
|
_ = await entry.ApiClient.UploadFileAsync(channelName, stream, fileName, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateChannelTopic(string serverUrl, string channelName, string? topic)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelModel? channel = entry.Server.Channels.FirstOrDefault(c =>
|
||||||
|
string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
_ = channel?.Topic = topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddChannelToList(string serverUrl, ChannelDto channelDto)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelModel model = ChannelModelFromDto(channelDto);
|
||||||
|
entry.Server.Channels.Add(model);
|
||||||
|
ChannelAdded?.Invoke(serverUrl, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveChannelFromList(string serverUrl, string channelName)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelModel? channel = entry.Server.Channels.FirstOrDefault(c =>
|
||||||
|
string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (channel is not null)
|
||||||
|
{
|
||||||
|
_ = entry.Server.Channels.Remove(channel);
|
||||||
|
ChannelRemoved?.Invoke(serverUrl, channelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetRefreshToken(string serverUrl)
|
||||||
|
{
|
||||||
|
return _connections.TryGetValue(serverUrl, out ServerConnection? entry)
|
||||||
|
? entry.ApiClient.RefreshToken
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> DownloadAttachmentAsync(string serverUrl, string relativeUrl, string fileName)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, fileName);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]?> DownloadImageBytesAsync(string serverUrl, string relativeUrl)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? tempPath = await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, "image");
|
||||||
|
if (tempPath is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = await File.ReadAllBytesAsync(tempPath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(tempPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore if deletion fails
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetCurrentUsername(string serverUrl)
|
||||||
|
{
|
||||||
|
return _connections.TryGetValue(serverUrl, out ServerConnection? entry)
|
||||||
|
? entry.User.DisplayName
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach ((string _, ServerConnection? entry) in _connections)
|
||||||
|
{
|
||||||
|
entry.ApiClient.Dispose();
|
||||||
|
}
|
||||||
|
_connections.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ChannelModel ChannelModelFromDto(ChannelDto dto)
|
||||||
|
{
|
||||||
|
return new ChannelModel(
|
||||||
|
dto.Id.ToString(),
|
||||||
|
dto.Name,
|
||||||
|
[],
|
||||||
|
dto.Topic,
|
||||||
|
dto.IsPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MessageModel MessageModelFromDto(MessageDto dto, ServerConnection entry)
|
||||||
|
{
|
||||||
|
UserModel author = new UserModel(
|
||||||
|
dto.SenderUsername,
|
||||||
|
dto.SenderUsername,
|
||||||
|
dto.SenderNicknameColor);
|
||||||
|
|
||||||
|
return new MessageModel(
|
||||||
|
dto.Id.ToString("N"),
|
||||||
|
author,
|
||||||
|
dto.SentAt,
|
||||||
|
dto.Content,
|
||||||
|
dto.ChannelName,
|
||||||
|
entry.Server.ServerUrl,
|
||||||
|
dto.Type,
|
||||||
|
dto.AttachmentUrl,
|
||||||
|
dto.AttachmentFileName,
|
||||||
|
dto.AttachmentFileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ServerConnection? GetConnection(string serverUrl)
|
||||||
|
{
|
||||||
|
return _connections.TryGetValue(serverUrl, out ServerConnection? conn) ? conn : null;
|
||||||
|
}
|
||||||
|
|
||||||
private void SaveRefreshToken(string serverUrl, bool rememberMe)
|
private void SaveRefreshToken(string serverUrl, bool rememberMe)
|
||||||
{
|
{
|
||||||
if (!rememberMe) return;
|
if (!rememberMe)
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry)) return;
|
{
|
||||||
var token = entry.ApiClient.RefreshToken;
|
return;
|
||||||
if (string.IsNullOrEmpty(token)) return;
|
}
|
||||||
|
|
||||||
var config = ConfigManager.Load();
|
if (!_connections.TryGetValue(serverUrl, out ServerConnection? entry))
|
||||||
var saved = config.SavedServers.FirstOrDefault(s =>
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? token = entry.ApiClient.RefreshToken;
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientConfig config = ConfigManager.Load();
|
||||||
|
SavedServer? saved = config.SavedServers.FirstOrDefault(s =>
|
||||||
string.Equals(s.Url, serverUrl, StringComparison.OrdinalIgnoreCase));
|
string.Equals(s.Url, serverUrl, StringComparison.OrdinalIgnoreCase));
|
||||||
if (saved is null)
|
if (saved is null)
|
||||||
{
|
{
|
||||||
@@ -149,280 +536,11 @@ public sealed class ConnectionService : IDisposable
|
|||||||
ConfigManager.Save(config);
|
ConfigManager.Save(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DisconnectAsync(string serverUrl)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
|
|
||||||
entry.Server.IsConnected = false;
|
|
||||||
entry.Server.IsConnecting = false;
|
|
||||||
ServerStateChanged?.Invoke(entry.Server);
|
|
||||||
|
|
||||||
await entry.Manager.CleanupAsync();
|
|
||||||
entry.ApiClient.Dispose();
|
|
||||||
await entry.Manager.DisposeAsync();
|
|
||||||
|
|
||||||
_connections.Remove(serverUrl);
|
|
||||||
ServerRemoved?.Invoke(serverUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendMessageAsync(string serverUrl, string channelName, string content)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
throw new InvalidOperationException("Not connected to server");
|
|
||||||
|
|
||||||
await entry.Manager.SendMessageAsync(channelName, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<MessageModel>> JoinChannelAsync(string serverUrl, string channelName)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
throw new InvalidOperationException("Not connected to server");
|
|
||||||
|
|
||||||
if (entry.Manager.TrackChannel(channelName))
|
|
||||||
{
|
|
||||||
var history = await entry.Manager.JoinChannelAsync(channelName);
|
|
||||||
return history.Select(m => MessageModelFromDto(m, entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var existing = await entry.Manager.GetHistoryAsync(channelName);
|
|
||||||
return existing.Select(m => MessageModelFromDto(m, entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LeaveChannelAsync(string serverUrl, string channelName)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.Manager.LeaveChannelAsync(channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<MessageModel>> GetHistoryAsync(string serverUrl, string channelName, int count = HubConstants.DefaultHistoryCount, int offset = 0)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var history = await entry.Manager.GetHistoryAsync(channelName, count, offset);
|
|
||||||
return history.Select(m => MessageModelFromDto(m, entry)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<UserPresenceDto>> GetOnlineUsersAsync(string serverUrl, string channelName)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return [];
|
|
||||||
return await entry.Manager.GetOnlineUsersAsync(channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateStatusAsync(string serverUrl, UserStatus status, string? statusMessage)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.Manager.UpdateStatusAsync(status, statusMessage);
|
|
||||||
entry.User.Status = status;
|
|
||||||
entry.User.StatusMessage = statusMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ChannelDto?> CreateChannelAsync(string serverUrl, string name, string? topic, bool isPublic)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return null;
|
|
||||||
return await entry.ApiClient.CreateChannelAsync(name, topic, isPublic);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteChannelAsync(string serverUrl, string channelName)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.DeleteChannelAsync(channelName);
|
|
||||||
entry.Manager.UntrackChannel(channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task KickUserAsync(string serverUrl, string username, string? reason)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.KickUserAsync(username, reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BanUserAsync(string serverUrl, string username, string? reason)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.BanUserAsync(username, reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnbanUserAsync(string serverUrl, string username)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.UnbanUserAsync(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task MuteUserAsync(string serverUrl, string username, int? durationMinutes)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.MuteUserAsync(username, durationMinutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnmuteUserAsync(string serverUrl, string username)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.UnmuteUserAsync(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AssignRoleAsync(string serverUrl, string username, ServerRole role)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.AssignRoleAsync(username, role);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task NukeChannelAsync(string serverUrl, string channelName)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.NukeChannelAsync(channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateProfileAsync(string serverUrl, string? displayName, string? bio, string? nickColor)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.UpdateProfileAsync(new UpdateProfileRequest(displayName, bio, nickColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetAvatarAsync(string serverUrl, string target)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await AvatarHelper.UploadAsync(entry.ApiClient, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendUrlAsync(string serverUrl, string channelName, string url, string? size)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
await entry.ApiClient.SendUrlAsync(channelName, url, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UploadFileAsync(string serverUrl, string channelName, string filePath, string? size)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await using var stream = File.OpenRead(filePath);
|
|
||||||
var fileName = Path.GetFileName(filePath);
|
|
||||||
await entry.ApiClient.UploadFileAsync(channelName, stream, fileName, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateChannelTopic(string serverUrl, string channelName, string? topic)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var channel = entry.Server.Channels.FirstOrDefault(c =>
|
|
||||||
string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (channel is not null)
|
|
||||||
channel.Topic = topic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddChannelToList(string serverUrl, ChannelDto channelDto)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var model = ChannelModelFromDto(channelDto);
|
|
||||||
entry.Server.Channels.Add(model);
|
|
||||||
ChannelAdded?.Invoke(serverUrl, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveChannelFromList(string serverUrl, string channelName)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var channel = entry.Server.Channels.FirstOrDefault(c =>
|
|
||||||
string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (channel is not null)
|
|
||||||
{
|
|
||||||
entry.Server.Channels.Remove(channel);
|
|
||||||
ChannelRemoved?.Invoke(serverUrl, channelName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientConfig LoadConfig() => ConfigManager.Load();
|
|
||||||
|
|
||||||
public void SaveConfig(ClientConfig config) => ConfigManager.Save(config);
|
|
||||||
|
|
||||||
public void SaveServerToConfig(SavedServer server) => ConfigManager.SaveServer(server);
|
|
||||||
|
|
||||||
public void RemoveServerFromConfig(string url) => ConfigManager.RemoveServer(url);
|
|
||||||
|
|
||||||
public CommandHandler CreateCommandHandler() => new();
|
|
||||||
|
|
||||||
public bool IsCommand(string input) => input.StartsWith('/');
|
|
||||||
|
|
||||||
public string? GetRefreshToken(string serverUrl)
|
|
||||||
{
|
|
||||||
return _connections.TryGetValue(serverUrl, out var entry)
|
|
||||||
? entry.ApiClient.RefreshToken
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> DownloadAttachmentAsync(string serverUrl, string relativeUrl, string fileName)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, fileName);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<byte[]?> DownloadImageBytesAsync(string serverUrl, string relativeUrl)
|
|
||||||
{
|
|
||||||
if (!_connections.TryGetValue(serverUrl, out var entry))
|
|
||||||
return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var tempPath = await entry.ApiClient.DownloadFileToTempAsync(relativeUrl, "image");
|
|
||||||
if (tempPath is null) return null;
|
|
||||||
var bytes = await File.ReadAllBytesAsync(tempPath);
|
|
||||||
try { File.Delete(tempPath); } catch { }
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? GetCurrentUsername(string serverUrl)
|
|
||||||
{
|
|
||||||
return _connections.TryGetValue(serverUrl, out var entry)
|
|
||||||
? entry.User.DisplayName
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var (_, entry) in _connections)
|
|
||||||
{
|
|
||||||
entry.ApiClient.Dispose();
|
|
||||||
}
|
|
||||||
_connections.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WireConnectionEvents(ServerConnection entry, ConnectionManager conn)
|
private void WireConnectionEvents(ServerConnection entry, ConnectionManager conn)
|
||||||
{
|
{
|
||||||
conn.MessageReceived += message =>
|
conn.MessageReceived += message =>
|
||||||
{
|
{
|
||||||
var msg = MessageModelFromDto(message, entry);
|
MessageModel msg = MessageModelFromDto(message, entry);
|
||||||
MessageReceived?.Invoke(entry.Server.ServerUrl, msg);
|
MessageReceived?.Invoke(entry.Server.ServerUrl, msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -444,12 +562,9 @@ public sealed class ConnectionService : IDisposable
|
|||||||
|
|
||||||
conn.ChannelUpdated += channel =>
|
conn.ChannelUpdated += channel =>
|
||||||
{
|
{
|
||||||
var existing = entry.Server.Channels.FirstOrDefault(c =>
|
ChannelModel? existing = entry.Server.Channels.FirstOrDefault(c =>
|
||||||
string.Equals(c.Name, channel.Name, StringComparison.OrdinalIgnoreCase));
|
string.Equals(c.Name, channel.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
if (existing is not null)
|
_ = existing?.Topic = channel.Topic;
|
||||||
{
|
|
||||||
existing.Topic = channel.Topic;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
conn.ForceDisconnected += reason =>
|
conn.ForceDisconnected += reason =>
|
||||||
@@ -471,44 +586,14 @@ public sealed class ConnectionService : IDisposable
|
|||||||
ServerStateChanged?.Invoke(entry.Server);
|
ServerStateChanged?.Invoke(entry.Server);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ChannelModel ChannelModelFromDto(ChannelDto dto)
|
|
||||||
{
|
|
||||||
return new ChannelModel(
|
|
||||||
dto.Id.ToString(),
|
|
||||||
dto.Name,
|
|
||||||
new ObservableCollection<MessageModel>(),
|
|
||||||
dto.Topic,
|
|
||||||
dto.IsPublic);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static MessageModel MessageModelFromDto(MessageDto dto, ServerConnection entry)
|
|
||||||
{
|
|
||||||
var author = new UserModel(
|
|
||||||
dto.SenderUsername,
|
|
||||||
dto.SenderUsername,
|
|
||||||
dto.SenderNicknameColor);
|
|
||||||
|
|
||||||
return new MessageModel(
|
|
||||||
dto.Id.ToString("N"),
|
|
||||||
author,
|
|
||||||
dto.SentAt,
|
|
||||||
dto.Content,
|
|
||||||
dto.ChannelName,
|
|
||||||
entry.Server.ServerUrl,
|
|
||||||
dto.Type,
|
|
||||||
dto.AttachmentUrl,
|
|
||||||
dto.AttachmentFileName,
|
|
||||||
dto.AttachmentFileSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class ServerConnection
|
internal sealed class ServerConnection
|
||||||
{
|
{
|
||||||
internal ConnectionManager Manager { get; }
|
|
||||||
public ApiClient ApiClient { get; }
|
public ApiClient ApiClient { get; }
|
||||||
public ServerModel Server { get; }
|
public ServerModel Server { get; }
|
||||||
public UserModel User { get; }
|
public UserModel User { get; }
|
||||||
|
internal ConnectionManager Manager { get; }
|
||||||
|
|
||||||
internal ServerConnection(ConnectionManager manager, ApiClient apiClient, ServerModel server, UserModel user)
|
internal ServerConnection(ConnectionManager manager, ApiClient apiClient, ServerModel server, UserModel user)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ using Avalonia.Controls.Templates;
|
|||||||
|
|
||||||
using Decho.ViewModels;
|
using Decho.ViewModels;
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace Decho;
|
namespace Decho;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a view model, returns the corresponding view if possible.
|
/// Given a view model, returns the corresponding view if possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -18,10 +18,12 @@ public class ViewLocator : IDataTemplate
|
|||||||
public Control? Build(object? param)
|
public Control? Build(object? param)
|
||||||
{
|
{
|
||||||
if (param is null)
|
if (param is null)
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
|
string name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
|
||||||
var type = Type.GetType(name);
|
Type? type = Type.GetType(name);
|
||||||
|
|
||||||
if (type != null)
|
if (type != null)
|
||||||
{
|
{
|
||||||
@@ -35,4 +37,4 @@ public class ViewLocator : IDataTemplate
|
|||||||
{
|
{
|
||||||
return data is ViewModelBase;
|
return data is ViewModelBase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,12 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Decho.Models;
|
using Decho.Models;
|
||||||
|
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
public sealed class ChannelViewModel : ViewModelBase
|
public sealed class ChannelViewModel(ChannelModel model) : ViewModelBase
|
||||||
{
|
{
|
||||||
public ChannelViewModel(ChannelModel model)
|
public ChannelModel Model { get; } = model;
|
||||||
{
|
|
||||||
Model = model;
|
|
||||||
Messages = new ObservableCollection<MessageViewModel>(
|
|
||||||
model.Messages.Select(message => new MessageViewModel(message)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelModel Model { get; }
|
|
||||||
|
|
||||||
public string Name => Model.Name;
|
public string Name => Model.Name;
|
||||||
|
|
||||||
@@ -36,7 +28,8 @@ public sealed class ChannelViewModel : ViewModelBase
|
|||||||
|
|
||||||
public bool IsPublic => Model.IsPublic;
|
public bool IsPublic => Model.IsPublic;
|
||||||
|
|
||||||
public ObservableCollection<MessageViewModel> Messages { get; }
|
public ObservableCollection<MessageViewModel> Messages { get; } = new ObservableCollection<MessageViewModel>(
|
||||||
|
model.Messages.Select(message => new MessageViewModel(message)));
|
||||||
|
|
||||||
public void ClearMessages()
|
public void ClearMessages()
|
||||||
{
|
{
|
||||||
@@ -49,9 +42,4 @@ public sealed class ChannelViewModel : ViewModelBase
|
|||||||
Model.Messages.Add(message);
|
Model.Messages.Add(message);
|
||||||
Messages.Add(new MessageViewModel(message));
|
Messages.Add(new MessageViewModel(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddMessageViewModel(MessageViewModel messageViewModel)
|
|
||||||
{
|
|
||||||
Messages.Add(messageViewModel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,72 +1,60 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using EchoHub.Client.Commands;
|
using EchoHub.Client.Commands;
|
||||||
using ReactiveUI;
|
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
public sealed class ChatViewModel : ViewModelBase
|
public sealed class ChatViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private ObservableCollection<MessageViewModel> _messages = new();
|
|
||||||
private string _channelTitle = "Select a channel";
|
|
||||||
private string? _channelTopic;
|
|
||||||
private bool _hasTopic;
|
|
||||||
private string _currentServerUrl = string.Empty;
|
|
||||||
private string _currentChannelName = string.Empty;
|
|
||||||
|
|
||||||
public ChatViewModel()
|
|
||||||
{
|
|
||||||
Composer = new MessageComposerViewModel();
|
|
||||||
Composer.CommandRequested += HandleCommandAsync;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableCollection<MessageViewModel> Messages
|
public ObservableCollection<MessageViewModel> Messages
|
||||||
{
|
{
|
||||||
get => _messages;
|
get;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _messages, value);
|
private set => this.RaiseAndSetIfChanged(ref field, value);
|
||||||
}
|
} = [];
|
||||||
|
|
||||||
public string ChannelTitle
|
public string ChannelTitle
|
||||||
{
|
{
|
||||||
get => _channelTitle;
|
get;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _channelTitle, value);
|
private set => this.RaiseAndSetIfChanged(ref field, value);
|
||||||
}
|
} = "Select a channel";
|
||||||
|
|
||||||
public string? ChannelTopic
|
public string? ChannelTopic
|
||||||
{
|
{
|
||||||
get => _channelTopic;
|
get;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
this.RaiseAndSetIfChanged(ref _channelTopic, value);
|
_ = this.RaiseAndSetIfChanged(ref field, value);
|
||||||
this.RaisePropertyChanged(nameof(HasTopic));
|
this.RaisePropertyChanged(nameof(HasTopic));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasTopic
|
public bool HasTopic
|
||||||
{
|
{
|
||||||
get => _hasTopic;
|
get;
|
||||||
private set => this.RaiseAndSetIfChanged(ref _hasTopic, value);
|
private set => this.RaiseAndSetIfChanged(ref field, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageComposerViewModel Composer { get; }
|
public MessageComposerViewModel Composer { get; }
|
||||||
|
|
||||||
public string CurrentServerUrl => _currentServerUrl;
|
public string CurrentServerUrl { get; private set; } = string.Empty;
|
||||||
public string CurrentChannelName => _currentChannelName;
|
|
||||||
|
|
||||||
public event Func<string, Task<string?>>? CommandRequested;
|
public string CurrentChannelName { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public ChatViewModel()
|
||||||
|
{
|
||||||
|
Composer = new MessageComposerViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
public void SetChannel(ChannelViewModel? channel, string serverUrl = "")
|
public void SetChannel(ChannelViewModel? channel, string serverUrl = "")
|
||||||
{
|
{
|
||||||
if (channel is null)
|
if (channel is null)
|
||||||
{
|
{
|
||||||
Messages = new ObservableCollection<MessageViewModel>();
|
Messages = [];
|
||||||
ChannelTitle = "Select a channel";
|
ChannelTitle = "Select a channel";
|
||||||
ChannelTopic = null;
|
ChannelTopic = null;
|
||||||
HasTopic = false;
|
HasTopic = false;
|
||||||
_currentChannelName = string.Empty;
|
CurrentChannelName = string.Empty;
|
||||||
_currentServerUrl = string.Empty;
|
CurrentServerUrl = string.Empty;
|
||||||
Composer.SetServer(string.Empty);
|
Composer.SetServer(string.Empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -75,8 +63,8 @@ public sealed class ChatViewModel : ViewModelBase
|
|||||||
ChannelTitle = "#" + channel.Name;
|
ChannelTitle = "#" + channel.Name;
|
||||||
ChannelTopic = channel.Topic;
|
ChannelTopic = channel.Topic;
|
||||||
HasTopic = channel.HasTopic;
|
HasTopic = channel.HasTopic;
|
||||||
_currentChannelName = channel.Name;
|
CurrentChannelName = channel.Name;
|
||||||
_currentServerUrl = serverUrl;
|
CurrentServerUrl = serverUrl;
|
||||||
Composer.SetServer(serverUrl);
|
Composer.SetServer(serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,25 +73,11 @@ public sealed class ChatViewModel : ViewModelBase
|
|||||||
Composer.SetCommandHandler(handler);
|
Composer.SetCommandHandler(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddMessage(MessageViewModel message)
|
|
||||||
{
|
|
||||||
Messages.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearMessages()
|
public void ClearMessages()
|
||||||
{
|
{
|
||||||
Messages = new ObservableCollection<MessageViewModel>();
|
Messages = [];
|
||||||
ChannelTitle = "Select a channel";
|
ChannelTitle = "Select a channel";
|
||||||
ChannelTopic = null;
|
ChannelTopic = null;
|
||||||
HasTopic = false;
|
HasTopic = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> HandleCommandAsync(string commandText)
|
|
||||||
{
|
|
||||||
if (CommandRequested is not null)
|
|
||||||
{
|
|
||||||
return await CommandRequested(commandText);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,4 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reactive;
|
|
||||||
using System.Reactive.Concurrency;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
|
|
||||||
using Decho.Models;
|
using Decho.Models;
|
||||||
using Decho.Services;
|
using Decho.Services;
|
||||||
@@ -19,16 +9,14 @@ using EchoHub.Core.Constants;
|
|||||||
using EchoHub.Core.DTOs;
|
using EchoHub.Core.DTOs;
|
||||||
using EchoHub.Core.Models;
|
using EchoHub.Core.Models;
|
||||||
|
|
||||||
using ReactiveUI;
|
using System.Reactive;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
public sealed class MainWindowViewModel : ViewModelBase
|
public sealed class MainWindowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ConnectionService _connectionService;
|
|
||||||
private readonly CommandHandler _commandHandler;
|
private readonly CommandHandler _commandHandler;
|
||||||
private string _statusText = "Ready";
|
|
||||||
private bool _isConnected;
|
|
||||||
private Window? _mainWindow;
|
private Window? _mainWindow;
|
||||||
|
|
||||||
public string Title { get; } = "Decho";
|
public string Title { get; } = "Decho";
|
||||||
@@ -39,24 +27,18 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string StatusText
|
public string StatusText
|
||||||
{
|
{
|
||||||
get => _statusText;
|
get;
|
||||||
set => this.RaiseAndSetIfChanged(ref _statusText, value);
|
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||||
}
|
} = "Ready";
|
||||||
|
|
||||||
public bool IsConnected
|
public ConnectionService ConnectionService { get; }
|
||||||
{
|
|
||||||
get => _isConnected;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _isConnected, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConnectionService ConnectionService => _connectionService;
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> AddServerCommand { get; }
|
public ReactiveCommand<Unit, Unit> AddServerCommand { get; }
|
||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
{
|
{
|
||||||
_connectionService = new ConnectionService();
|
ConnectionService = new ConnectionService();
|
||||||
_commandHandler = _connectionService.CreateCommandHandler();
|
_commandHandler = new CommandHandler();
|
||||||
|
|
||||||
Sidebar = new SidebarViewModel();
|
Sidebar = new SidebarViewModel();
|
||||||
Chat = new ChatViewModel();
|
Chat = new ChatViewModel();
|
||||||
@@ -70,8 +52,8 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
WireCommandHandlerEvents();
|
WireCommandHandlerEvents();
|
||||||
WireConnectionServiceEvents();
|
WireConnectionServiceEvents();
|
||||||
|
|
||||||
Sidebar.WhenAnyValue(x => x.SelectedChannel)
|
_ = Sidebar.WhenAnyValue(x => x.SelectedChannel)
|
||||||
.Subscribe(channel => HandleChannelSelected(channel));
|
.Subscribe(HandleChannelSelected);
|
||||||
|
|
||||||
_ = InitializeSavedServersAsync();
|
_ = InitializeSavedServersAsync();
|
||||||
}
|
}
|
||||||
@@ -81,15 +63,20 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
_mainWindow = window;
|
_mainWindow = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ConnectionService.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private void AddServer()
|
private void AddServer()
|
||||||
{
|
{
|
||||||
var placeholderServer = new ServerModel(
|
ServerModel placeholderServer = new ServerModel(
|
||||||
Guid.NewGuid().ToString("N"),
|
Guid.NewGuid().ToString("N"),
|
||||||
"New Server",
|
"New Server",
|
||||||
new ObservableCollection<ChannelModel>(),
|
[],
|
||||||
isConnected: false);
|
isConnected: false);
|
||||||
|
|
||||||
var 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);
|
||||||
|
|
||||||
@@ -101,9 +88,13 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_commandHandler.OnSetStatus += async (status, message) =>
|
_commandHandler.OnSetStatus += async (status, message) =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.UpdateStatusAsync(serverUrl, status, message);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.UpdateStatusAsync(serverUrl, status, message);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnSetTheme += themeName =>
|
_commandHandler.OnSetTheme += themeName =>
|
||||||
@@ -113,111 +104,156 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
_commandHandler.OnJoinChannel += async channelName =>
|
_commandHandler.OnJoinChannel += async channelName =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var channel = await _connectionService.JoinChannelAsync(serverUrl, channelName);
|
List<MessageModel> channel = await ConnectionService.JoinChannelAsync(serverUrl, channelName);
|
||||||
EnsureChannelInList(serverUrl, channelName);
|
EnsureChannelInList(serverUrl, channelName);
|
||||||
var channelModel = FindChannel(serverUrl, channelName);
|
ChannelModel? channelModel = FindChannel(serverUrl, channelName);
|
||||||
if (channelModel is not null)
|
if (channelModel is not null)
|
||||||
{
|
{
|
||||||
var channelVm = Sidebar.GetServer(serverUrl)?.Channels
|
ChannelViewModel? channelVm = Sidebar.GetServer(serverUrl)?.Channels
|
||||||
.FirstOrDefault(c => c.Name == channelName);
|
.FirstOrDefault(c => c.Name == channelName);
|
||||||
if (channelVm is not null)
|
if (channelVm is not null)
|
||||||
{
|
{
|
||||||
foreach (var msg in channel)
|
foreach (MessageModel msg in channel)
|
||||||
|
{
|
||||||
channelVm.AddMessage(msg);
|
channelVm.AddMessage(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnLeaveChannel += async () =>
|
_commandHandler.OnLeaveChannel += async () =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
var channel = Chat.CurrentChannelName;
|
string channel = Chat.CurrentChannelName;
|
||||||
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return;
|
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (channel == HubConstants.DefaultChannel) return;
|
if (channel == HubConstants.DefaultChannel)
|
||||||
await _connectionService.LeaveChannelAsync(serverUrl, channel);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.LeaveChannelAsync(serverUrl, channel);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnListUsers += async () =>
|
_commandHandler.OnListUsers += async () =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
var channel = Chat.CurrentChannelName;
|
string channel = Chat.CurrentChannelName;
|
||||||
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return;
|
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var users = await _connectionService.GetOnlineUsersAsync(serverUrl, channel);
|
List<UserPresenceDto> users = await ConnectionService.GetOnlineUsersAsync(serverUrl, channel);
|
||||||
var userList = string.Join(", ", users.Select(u => u.DisplayName ?? u.Username));
|
string userList = string.Join(", ", users.Select(u => u.DisplayName ?? u.Username));
|
||||||
StatusText = $"Online in #{channel}: {userList}";
|
StatusText = $"Online in #{channel}: {userList}";
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnSetTopic += async topic =>
|
_commandHandler.OnSetTopic += async topic =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
var channel = Chat.CurrentChannelName;
|
string channel = Chat.CurrentChannelName;
|
||||||
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return;
|
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _connectionService.UpdateProfileAsync(serverUrl, null, null, null);
|
await ConnectionService.UpdateProfileAsync(serverUrl, null, null, null);
|
||||||
_connectionService.UpdateChannelTopic(serverUrl, channel, topic);
|
ConnectionService.UpdateChannelTopic(serverUrl, channel, topic);
|
||||||
Chat.ChannelTopic = topic;
|
Chat.ChannelTopic = topic;
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnKickUser += async (username, reason) =>
|
_commandHandler.OnKickUser += async (username, reason) =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.KickUserAsync(serverUrl, username, reason);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.KickUserAsync(serverUrl, username, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnBanUser += async (username, reason) =>
|
_commandHandler.OnBanUser += async (username, reason) =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.BanUserAsync(serverUrl, username, reason);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.BanUserAsync(serverUrl, username, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnUnbanUser += async username =>
|
_commandHandler.OnUnbanUser += async username =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.UnbanUserAsync(serverUrl, username);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.UnbanUserAsync(serverUrl, username);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnMuteUser += async (username, duration) =>
|
_commandHandler.OnMuteUser += async (username, duration) =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.MuteUserAsync(serverUrl, username, duration);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.MuteUserAsync(serverUrl, username, duration);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnUnmuteUser += async username =>
|
_commandHandler.OnUnmuteUser += async username =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.UnmuteUserAsync(serverUrl, username);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.UnmuteUserAsync(serverUrl, username);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnAssignRole += async (username, roleStr) =>
|
_commandHandler.OnAssignRole += async (username, roleStr) =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var role = roleStr.ToLowerInvariant() switch
|
ServerRole role = roleStr.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"admin" => ServerRole.Admin,
|
"admin" => ServerRole.Admin,
|
||||||
"mod" => ServerRole.Mod,
|
"mod" => ServerRole.Mod,
|
||||||
_ => ServerRole.Member,
|
_ => ServerRole.Member,
|
||||||
};
|
};
|
||||||
await _connectionService.AssignRoleAsync(serverUrl, username, role);
|
await ConnectionService.AssignRoleAsync(serverUrl, username, role);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnNukeChannel += async () =>
|
_commandHandler.OnNukeChannel += async () =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
var channel = Chat.CurrentChannelName;
|
string channel = Chat.CurrentChannelName;
|
||||||
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return;
|
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel))
|
||||||
await _connectionService.NukeChannelAsync(serverUrl, channel);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.NukeChannelAsync(serverUrl, channel);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnTestSound += () => Task.CompletedTask;
|
_commandHandler.OnTestSound += () => Task.CompletedTask;
|
||||||
@@ -225,7 +261,10 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
_commandHandler.OnQuit += () =>
|
_commandHandler.OnQuit += () =>
|
||||||
{
|
{
|
||||||
if (_mainWindow is not null)
|
if (_mainWindow is not null)
|
||||||
|
{
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() => _mainWindow.Close());
|
Avalonia.Threading.Dispatcher.UIThread.Post(() => _mainWindow.Close());
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,20 +272,23 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
_commandHandler.OnSendFile += async (target, size) =>
|
_commandHandler.OnSendFile += async (target, size) =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
var channel = Chat.CurrentChannelName;
|
string channel = Chat.CurrentChannelName;
|
||||||
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel)) return;
|
if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(channel))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Uri.TryCreate(target, UriKind.Absolute, out var uri)
|
if (Uri.TryCreate(target, UriKind.Absolute, out Uri? uri)
|
||||||
&& (uri.Scheme == "http" || uri.Scheme == "https"))
|
&& (uri.Scheme == "http" || uri.Scheme == "https"))
|
||||||
{
|
{
|
||||||
await _connectionService.SendUrlAsync(serverUrl, channel, target, size);
|
await ConnectionService.SendUrlAsync(serverUrl, channel, target, size);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _connectionService.UploadFileAsync(serverUrl, channel, target, size);
|
await ConnectionService.UploadFileAsync(serverUrl, channel, target, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -257,23 +299,35 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
_commandHandler.OnSetNick += async displayName =>
|
_commandHandler.OnSetNick += async displayName =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.UpdateProfileAsync(serverUrl, displayName, null, null);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.UpdateProfileAsync(serverUrl, displayName, null, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnSetColor += async color =>
|
_commandHandler.OnSetColor += async color =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.UpdateProfileAsync(serverUrl, null, null, color);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.UpdateProfileAsync(serverUrl, null, null, color);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnSetAvatar += async target =>
|
_commandHandler.OnSetAvatar += async target =>
|
||||||
{
|
{
|
||||||
var serverUrl = GetCurrentServerUrl();
|
string serverUrl = GetCurrentServerUrl();
|
||||||
if (string.IsNullOrEmpty(serverUrl)) return;
|
if (string.IsNullOrEmpty(serverUrl))
|
||||||
await _connectionService.SetAvatarAsync(serverUrl, target);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConnectionService.SetAvatarAsync(serverUrl, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
_commandHandler.OnOpenProfile += async username =>
|
_commandHandler.OnOpenProfile += async username =>
|
||||||
@@ -283,8 +337,8 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
_commandHandler.OnOpenServers += () =>
|
_commandHandler.OnOpenServers += () =>
|
||||||
{
|
{
|
||||||
var config = _connectionService.LoadConfig();
|
ClientConfig config = ConfigManager.Load();
|
||||||
var servers = string.Join("\n", config.SavedServers.Select(s =>
|
string servers = string.Join("\n", config.SavedServers.Select(s =>
|
||||||
$"{s.Name} ({s.Url}) - {s.Username ?? "?"}"));
|
$"{s.Name} ({s.Url}) - {s.Username ?? "?"}"));
|
||||||
StatusText = servers;
|
StatusText = servers;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -293,75 +347,67 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
private void WireConnectionServiceEvents()
|
private void WireConnectionServiceEvents()
|
||||||
{
|
{
|
||||||
_connectionService.ServerAdded += server =>
|
ConnectionService.ServerAdded += server =>
|
||||||
{
|
{
|
||||||
var 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.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!);
|
||||||
|
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
var existing = Sidebar.GetServer(server.ServerUrl);
|
ServerViewModel? existing = Sidebar.GetServer(server.ServerUrl);
|
||||||
if (existing is not null)
|
if (existing is not null)
|
||||||
Sidebar.Servers.Remove(existing);
|
{
|
||||||
|
_ = Sidebar.Servers.Remove(existing);
|
||||||
|
}
|
||||||
|
|
||||||
Sidebar.Servers.Add(serverVm);
|
Sidebar.Servers.Add(serverVm);
|
||||||
StatusText = $"Connected to {server.Name}";
|
StatusText = $"Connected to {server.Name}";
|
||||||
IsConnected = true;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_connectionService.ServerRemoved += serverUrl =>
|
ConnectionService.ServerRemoved += serverUrl =>
|
||||||
{
|
{
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
Sidebar.RemoveServer(serverUrl);
|
Sidebar.RemoveServer(serverUrl);
|
||||||
if (Sidebar.Servers.Count == 0)
|
if (Sidebar.Servers.Count == 0)
|
||||||
{
|
{
|
||||||
IsConnected = false;
|
|
||||||
Chat.ClearMessages();
|
Chat.ClearMessages();
|
||||||
StatusText = "Ready";
|
StatusText = "Ready";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_connectionService.ServerStateChanged += server =>
|
ConnectionService.ServerStateChanged += server =>
|
||||||
{
|
{
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
var serverVm = Sidebar.GetServer(server.ServerUrl);
|
ServerViewModel? serverVm = Sidebar.GetServer(server.ServerUrl);
|
||||||
if (serverVm is not null)
|
if (serverVm is not null)
|
||||||
{
|
{
|
||||||
serverVm.SyncFromModel();
|
serverVm.SyncFromModel();
|
||||||
if (!server.IsConnected)
|
if (!server.IsConnected)
|
||||||
|
{
|
||||||
StatusText = $"Disconnected from {server.Name}";
|
StatusText = $"Disconnected from {server.Name}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_connectionService.MessageReceived += (serverUrl, message) =>
|
ConnectionService.MessageReceived += (serverUrl, message) =>
|
||||||
{
|
{
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
var channelVm = FindChannelViewModel(serverUrl, message.ChannelName);
|
ChannelViewModel? channelVm = FindChannelViewModel(serverUrl, message.ChannelName);
|
||||||
if (channelVm is not null)
|
channelVm?.AddMessage(message);
|
||||||
{
|
|
||||||
channelVm.AddMessage(message);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_connectionService.ChannelAdded += (serverUrl, channel) =>
|
ConnectionService.ErrorOccurred += (serverUrl, error) =>
|
||||||
{
|
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
EnsureChannelInList(serverUrl, channel.Name);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_connectionService.ErrorOccurred += (serverUrl, error) =>
|
|
||||||
{
|
{
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
@@ -372,10 +418,10 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
private async Task<string?> HandleCommandAsync(string commandText)
|
private async Task<string?> HandleCommandAsync(string commandText)
|
||||||
{
|
{
|
||||||
var result = await _commandHandler.HandleAsync(commandText);
|
CommandResult result = await _commandHandler.HandleAsync(commandText);
|
||||||
if (result.Message is not null && !result.IsError)
|
if (result.Message is not null && !result.IsError)
|
||||||
{
|
{
|
||||||
Chat.AddMessage(new MessageViewModel(new MessageModel(
|
Chat.Messages.Add(new MessageViewModel(new MessageModel(
|
||||||
Guid.NewGuid().ToString("N"),
|
Guid.NewGuid().ToString("N"),
|
||||||
new UserModel("system", "System"),
|
new UserModel("system", "System"),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
@@ -389,19 +435,21 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
private void HandleSendRequested(string serverUrl, string text)
|
private void HandleSendRequested(string serverUrl, string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(Chat.CurrentChannelName))
|
if (string.IsNullOrEmpty(Chat.CurrentChannelName))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_commandHandler.IsCommand(text))
|
if (_commandHandler.IsCommand(text))
|
||||||
{
|
{
|
||||||
_ = HandleCommandAsync(text);
|
_ = HandleCommandAsync(text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _connectionService.SendMessageAsync(serverUrl, Chat.CurrentChannelName, text);
|
await ConnectionService.SendMessageAsync(serverUrl, Chat.CurrentChannelName, text);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -415,13 +463,16 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
private void HandleFileUploadRequested(string serverUrl, string filePath)
|
private void HandleFileUploadRequested(string serverUrl, string filePath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(Chat.CurrentChannelName)) return;
|
if (string.IsNullOrEmpty(Chat.CurrentChannelName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _connectionService.UploadFileAsync(serverUrl, Chat.CurrentChannelName, filePath, null);
|
await ConnectionService.UploadFileAsync(serverUrl, Chat.CurrentChannelName, filePath, null);
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
StatusText = "File uploaded");
|
StatusText = "File uploaded");
|
||||||
}
|
}
|
||||||
@@ -441,24 +492,25 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverUrl = FindServerUrlForChannel(channel);
|
string serverUrl = FindServerUrlForChannel(channel);
|
||||||
Chat.SetChannel(channel, serverUrl);
|
Chat.SetChannel(channel, serverUrl);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(serverUrl))
|
if (!string.IsNullOrEmpty(serverUrl))
|
||||||
{
|
{
|
||||||
var commandHandler = _connectionService.CreateCommandHandler();
|
Chat.SetComposerCommandHandler(new CommandHandler());
|
||||||
Chat.SetComposerCommandHandler(commandHandler);
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var history = await _connectionService.JoinChannelAsync(serverUrl, channel.Name);
|
List<MessageModel> history = await ConnectionService.JoinChannelAsync(serverUrl, channel.Name);
|
||||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
channel.ClearMessages();
|
channel.ClearMessages();
|
||||||
foreach (var msg in history)
|
foreach (MessageModel msg in history)
|
||||||
|
{
|
||||||
channel.AddMessage(msg);
|
channel.AddMessage(msg);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -471,19 +523,19 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
private async Task InitializeSavedServersAsync()
|
private async Task InitializeSavedServersAsync()
|
||||||
{
|
{
|
||||||
var config = _connectionService.LoadConfig();
|
ClientConfig config = ConfigManager.Load();
|
||||||
var connectTasks = new List<Task>();
|
List<Task> connectTasks = [];
|
||||||
|
|
||||||
foreach (var saved in config.SavedServers)
|
foreach (SavedServer saved in config.SavedServers)
|
||||||
{
|
{
|
||||||
var placeholderServer = new ServerModel(
|
ServerModel placeholderServer = new ServerModel(
|
||||||
Guid.NewGuid().ToString("N"),
|
Guid.NewGuid().ToString("N"),
|
||||||
saved.Name,
|
saved.Name,
|
||||||
new ObservableCollection<ChannelModel>(),
|
[],
|
||||||
saved.Url,
|
saved.Url,
|
||||||
isConnected: false);
|
isConnected: false);
|
||||||
|
|
||||||
var 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);
|
||||||
|
|
||||||
@@ -504,12 +556,14 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
private async Task AutoConnectSavedServer(ServerViewModel serverVm, SavedServer saved)
|
private async Task AutoConnectSavedServer(ServerViewModel serverVm, SavedServer saved)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(saved.Username) || string.IsNullOrEmpty(saved.RefreshToken))
|
if (string.IsNullOrEmpty(saved.Username) || string.IsNullOrEmpty(saved.RefreshToken))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
serverVm.IsConnecting = true;
|
serverVm.IsConnecting = true;
|
||||||
await _connectionService.ConnectWithSavedTokenAsync(
|
await ConnectionService.ConnectWithSavedTokenAsync(
|
||||||
saved.Url, saved.Username, saved.RefreshToken, saved.RememberMe);
|
saved.Url, saved.Username, saved.RefreshToken, saved.RememberMe);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -518,43 +572,24 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task ShowMessageBox(Window owner, string title, string message)
|
|
||||||
{
|
|
||||||
var msgBox = new Window
|
|
||||||
{
|
|
||||||
Title = title,
|
|
||||||
Width = 400,
|
|
||||||
Height = 180,
|
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
||||||
CanResize = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
var stack = new StackPanel { Spacing = 10, Margin = new Avalonia.Thickness(15) };
|
|
||||||
stack.Children.Add(new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap });
|
|
||||||
var okBtn = new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
|
|
||||||
stack.Children.Add(okBtn);
|
|
||||||
msgBox.Content = stack;
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
|
||||||
okBtn.Click += (_, _) => { msgBox.Close(); tcs.TrySetResult(); };
|
|
||||||
msgBox.Closed += (_, _) => tcs.TrySetResult();
|
|
||||||
|
|
||||||
await msgBox.ShowDialog(owner);
|
|
||||||
await tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleServerConnectRequested(ServerViewModel serverVm)
|
private async Task HandleServerConnectRequested(ServerViewModel serverVm)
|
||||||
{
|
{
|
||||||
// Show connect dialog
|
// Show connect dialog
|
||||||
if (_mainWindow is null) return;
|
if (_mainWindow is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var config = _connectionService.LoadConfig();
|
ClientConfig config = ConfigManager.Load();
|
||||||
var prefill = config.SavedServers.FirstOrDefault(s =>
|
SavedServer? prefill = config.SavedServers.FirstOrDefault(s =>
|
||||||
string.Equals(s.Url, serverVm.ServerUrl, StringComparison.OrdinalIgnoreCase));
|
string.Equals(s.Url, serverVm.ServerUrl, StringComparison.OrdinalIgnoreCase));
|
||||||
var dialog = new ConnectDialog(config.SavedServers, prefill);
|
ConnectDialog dialog = new ConnectDialog(config.SavedServers, prefill);
|
||||||
|
|
||||||
var result = await dialog.ShowAsync(_mainWindow);
|
ConnectDialogResult? result = await dialog.ShowAsync(_mainWindow);
|
||||||
if (result is null) return;
|
if (result is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -563,12 +598,12 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
if (result.IsSavedSession && result.SavedRefreshToken is not null)
|
if (result.IsSavedSession && result.SavedRefreshToken is not null)
|
||||||
{
|
{
|
||||||
await _connectionService.ConnectWithSavedTokenAsync(
|
await ConnectionService.ConnectWithSavedTokenAsync(
|
||||||
result.ServerUrl, result.Username, result.SavedRefreshToken, result.RememberMe);
|
result.ServerUrl, result.Username, result.SavedRefreshToken, result.RememberMe);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _connectionService.ConnectAsync(
|
_ = await ConnectionService.ConnectAsync(
|
||||||
result.ServerUrl, result.Username, result.Password, result.IsRegister, result.RememberMe);
|
result.ServerUrl, result.Username, result.Password, result.IsRegister, result.RememberMe);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,8 +611,8 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
Sidebar.RemoveServer(serverVm.ServerUrl);
|
Sidebar.RemoveServer(serverVm.ServerUrl);
|
||||||
|
|
||||||
// Save to config with refresh token
|
// Save to config with refresh token
|
||||||
var refreshToken = _connectionService.GetRefreshToken(result.ServerUrl);
|
string? refreshToken = ConnectionService.GetRefreshToken(result.ServerUrl);
|
||||||
var savedServer = new SavedServer
|
SavedServer savedServer = new SavedServer
|
||||||
{
|
{
|
||||||
Name = new Uri(result.ServerUrl).Host,
|
Name = new Uri(result.ServerUrl).Host,
|
||||||
Url = result.ServerUrl,
|
Url = result.ServerUrl,
|
||||||
@@ -586,7 +621,7 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
RememberMe = result.RememberMe,
|
RememberMe = result.RememberMe,
|
||||||
LastConnected = DateTimeOffset.Now,
|
LastConnected = DateTimeOffset.Now,
|
||||||
};
|
};
|
||||||
_connectionService.SaveServerToConfig(savedServer);
|
ConfigManager.SaveServer(savedServer);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -599,7 +634,7 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _connectionService.DisconnectAsync(serverVm.ServerUrl);
|
await ConnectionService.DisconnectAsync(serverVm.ServerUrl);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -609,38 +644,43 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
private void EnsureChannelInList(string serverUrl, string channelName)
|
private void EnsureChannelInList(string serverUrl, string channelName)
|
||||||
{
|
{
|
||||||
var serverVm = Sidebar.GetServer(serverUrl);
|
ServerViewModel? serverVm = Sidebar.GetServer(serverUrl);
|
||||||
if (serverVm is null) return;
|
if (serverVm is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!serverVm.Channels.Any(c => c.Name == channelName))
|
if (!serverVm.Channels.Any(c => c.Name == channelName))
|
||||||
{
|
{
|
||||||
var channelModel = new ChannelModel(
|
ChannelModel channelModel = new ChannelModel(
|
||||||
Guid.NewGuid().ToString("N"),
|
Guid.NewGuid().ToString("N"),
|
||||||
channelName,
|
channelName,
|
||||||
new System.Collections.ObjectModel.ObservableCollection<MessageModel>());
|
[]);
|
||||||
var channelVm = new ChannelViewModel(channelModel);
|
ChannelViewModel channelVm = new ChannelViewModel(channelModel);
|
||||||
serverVm.Channels.Add(channelVm);
|
serverVm.Channels.Add(channelVm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChannelModel? FindChannel(string serverUrl, string channelName)
|
private ChannelModel? FindChannel(string serverUrl, string channelName)
|
||||||
{
|
{
|
||||||
var serverVm = Sidebar.GetServer(serverUrl);
|
ServerViewModel? serverVm = Sidebar.GetServer(serverUrl);
|
||||||
return serverVm?.Channels.FirstOrDefault(c => c.Name == channelName)?.Model;
|
return serverVm?.Channels.FirstOrDefault(c => c.Name == channelName)?.Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChannelViewModel? FindChannelViewModel(string serverUrl, string channelName)
|
private ChannelViewModel? FindChannelViewModel(string serverUrl, string channelName)
|
||||||
{
|
{
|
||||||
var serverVm = Sidebar.GetServer(serverUrl);
|
ServerViewModel? serverVm = Sidebar.GetServer(serverUrl);
|
||||||
return serverVm?.Channels.FirstOrDefault(c => c.Name == channelName);
|
return serverVm?.Channels.FirstOrDefault(c => c.Name == channelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FindServerUrlForChannel(ChannelViewModel channel)
|
private string FindServerUrlForChannel(ChannelViewModel channel)
|
||||||
{
|
{
|
||||||
foreach (var server in Sidebar.Servers)
|
foreach (ServerViewModel server in Sidebar.Servers)
|
||||||
{
|
{
|
||||||
if (server.Channels.Contains(channel))
|
if (server.Channels.Contains(channel))
|
||||||
|
{
|
||||||
return server.ServerUrl;
|
return server.ServerUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
@@ -649,11 +689,6 @@ public sealed class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
return Chat.CurrentServerUrl;
|
return Chat.CurrentServerUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_connectionService.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ConnectDialogResult
|
public sealed class ConnectDialogResult
|
||||||
@@ -667,20 +702,14 @@ public sealed class ConnectDialogResult
|
|||||||
public string? SavedRefreshToken { get; set; }
|
public string? SavedRefreshToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ConnectDialog
|
public sealed class ConnectDialog(List<SavedServer> savedServers, SavedServer? prefill = null)
|
||||||
{
|
{
|
||||||
private readonly List<SavedServer> _savedServers;
|
private readonly List<SavedServer> _savedServers = savedServers;
|
||||||
private readonly SavedServer? _prefill;
|
private readonly SavedServer? _prefill = prefill;
|
||||||
|
|
||||||
public ConnectDialog(List<SavedServer> savedServers, SavedServer? prefill = null)
|
|
||||||
{
|
|
||||||
_savedServers = savedServers;
|
|
||||||
_prefill = prefill;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ConnectDialogResult?> ShowAsync(Window owner)
|
public async Task<ConnectDialogResult?> ShowAsync(Window owner)
|
||||||
{
|
{
|
||||||
var dialog = new Window
|
Window dialog = new Window
|
||||||
{
|
{
|
||||||
Title = "Connect to Server",
|
Title = "Connect to Server",
|
||||||
Width = 450,
|
Width = 450,
|
||||||
@@ -689,31 +718,31 @@ public sealed class ConnectDialog
|
|||||||
CanResize = false,
|
CanResize = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
var stack = new StackPanel { Spacing = 8, Margin = new Avalonia.Thickness(15) };
|
StackPanel stack = new StackPanel { Spacing = 8, Margin = new Avalonia.Thickness(15) };
|
||||||
|
|
||||||
var urlLabel = new TextBlock { Text = "Server URL:" };
|
TextBlock urlLabel = new TextBlock { Text = "Server URL:" };
|
||||||
var urlBox = new TextBox { Watermark = "http://localhost:5000", Text = "http://localhost:5000" };
|
TextBox urlBox = new TextBox { Watermark = "http://localhost:5000", Text = "http://localhost:5000" };
|
||||||
stack.Children.Add(urlLabel);
|
stack.Children.Add(urlLabel);
|
||||||
stack.Children.Add(urlBox);
|
stack.Children.Add(urlBox);
|
||||||
|
|
||||||
var userLabel = new TextBlock { Text = "Username:" };
|
TextBlock userLabel = new TextBlock { Text = "Username:" };
|
||||||
var userBox = new TextBox { Watermark = "username" };
|
TextBox userBox = new TextBox { Watermark = "username" };
|
||||||
stack.Children.Add(userLabel);
|
stack.Children.Add(userLabel);
|
||||||
stack.Children.Add(userBox);
|
stack.Children.Add(userBox);
|
||||||
|
|
||||||
var passLabel = new TextBlock { Text = "Password:" };
|
TextBlock passLabel = new TextBlock { Text = "Password:" };
|
||||||
var passBox = new TextBox { Watermark = "password", PasswordChar = '*' };
|
TextBox passBox = new TextBox { Watermark = "password", PasswordChar = '*' };
|
||||||
stack.Children.Add(passLabel);
|
stack.Children.Add(passLabel);
|
||||||
stack.Children.Add(passBox);
|
stack.Children.Add(passBox);
|
||||||
|
|
||||||
var rememberMe = new CheckBox { Content = "Remember me", IsChecked = true };
|
CheckBox rememberMe = new CheckBox { Content = "Remember me", IsChecked = true };
|
||||||
stack.Children.Add(rememberMe);
|
stack.Children.Add(rememberMe);
|
||||||
|
|
||||||
var buttonPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, Spacing = 10 };
|
StackPanel buttonPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, Spacing = 10 };
|
||||||
|
|
||||||
var loginBtn = new Button { Content = "Login", Width = 100 };
|
Button loginBtn = new Button { Content = "Login", Width = 100 };
|
||||||
var registerBtn = new Button { Content = "Register", Width = 100 };
|
Button registerBtn = new Button { Content = "Register", Width = 100 };
|
||||||
var cancelBtn = new Button { Content = "Cancel", Width = 100 };
|
Button cancelBtn = new Button { Content = "Cancel", Width = 100 };
|
||||||
buttonPanel.Children.Add(loginBtn);
|
buttonPanel.Children.Add(loginBtn);
|
||||||
buttonPanel.Children.Add(registerBtn);
|
buttonPanel.Children.Add(registerBtn);
|
||||||
buttonPanel.Children.Add(cancelBtn);
|
buttonPanel.Children.Add(cancelBtn);
|
||||||
@@ -722,24 +751,29 @@ public sealed class ConnectDialog
|
|||||||
ListBox? serversList = null;
|
ListBox? serversList = null;
|
||||||
if (_savedServers.Count > 0)
|
if (_savedServers.Count > 0)
|
||||||
{
|
{
|
||||||
var sep = new TextBlock { Text = "--- Saved Servers ---", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
|
TextBlock sep = new TextBlock { Text = "--- Saved Servers ---", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
|
||||||
stack.Children.Add(sep);
|
stack.Children.Add(sep);
|
||||||
|
|
||||||
serversList = new ListBox { Height = 100 };
|
serversList = new ListBox
|
||||||
serversList.ItemsSource = _savedServers.Select(s => $"{s.Name} ({s.Url}) - {s.Username ?? "?"}").ToList();
|
{
|
||||||
|
Height = 100,
|
||||||
|
ItemsSource = _savedServers.Select(s => $"{s.Name} ({s.Url}) - {s.Username ?? "?"}").ToList()
|
||||||
|
};
|
||||||
stack.Children.Add(serversList);
|
stack.Children.Add(serversList);
|
||||||
|
|
||||||
serversList.SelectionChanged += (_, _) =>
|
serversList.SelectionChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
var idx = serversList.SelectedIndex;
|
int idx = serversList.SelectedIndex;
|
||||||
if (idx >= 0 && idx < _savedServers.Count)
|
if (idx >= 0 && idx < _savedServers.Count)
|
||||||
{
|
{
|
||||||
var s = _savedServers[idx];
|
SavedServer s = _savedServers[idx];
|
||||||
urlBox.Text = s.Url;
|
urlBox.Text = s.Url;
|
||||||
userBox.Text = s.Username ?? "";
|
userBox.Text = s.Username ?? "";
|
||||||
rememberMe.IsChecked = s.RememberMe;
|
rememberMe.IsChecked = s.RememberMe;
|
||||||
if (!string.IsNullOrEmpty(s.RefreshToken))
|
if (!string.IsNullOrEmpty(s.RefreshToken))
|
||||||
|
{
|
||||||
passBox.Text = "";
|
passBox.Text = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -748,22 +782,24 @@ public sealed class ConnectDialog
|
|||||||
{
|
{
|
||||||
urlBox.Text = _prefill.Url;
|
urlBox.Text = _prefill.Url;
|
||||||
userBox.Text = _prefill.Username ?? "";
|
userBox.Text = _prefill.Username ?? "";
|
||||||
var idx = _savedServers.FindIndex(s =>
|
int idx = _savedServers.FindIndex(s =>
|
||||||
string.Equals(s.Url, _prefill.Url, StringComparison.OrdinalIgnoreCase));
|
string.Equals(s.Url, _prefill.Url, StringComparison.OrdinalIgnoreCase));
|
||||||
if (idx >= 0 && idx < _savedServers.Count && serversList is not null)
|
if (idx >= 0 && idx < _savedServers.Count && serversList is not null)
|
||||||
|
{
|
||||||
serversList.SelectedIndex = idx;
|
serversList.SelectedIndex = idx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.Content = new ScrollViewer { Content = stack };
|
dialog.Content = new ScrollViewer { Content = stack };
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<ConnectDialogResult?>();
|
TaskCompletionSource<ConnectDialogResult?> tcs = new TaskCompletionSource<ConnectDialogResult?>();
|
||||||
ConnectDialogResult? result = null;
|
ConnectDialogResult? result = null;
|
||||||
|
|
||||||
loginBtn.Click += async (_, _) =>
|
loginBtn.Click += async (_, _) =>
|
||||||
{
|
{
|
||||||
var url = urlBox.Text?.Trim() ?? "";
|
string url = urlBox.Text?.Trim() ?? "";
|
||||||
var user = userBox.Text?.Trim() ?? "";
|
string user = userBox.Text?.Trim() ?? "";
|
||||||
var pass = passBox.Text ?? "";
|
string pass = passBox.Text ?? "";
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user))
|
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user))
|
||||||
{
|
{
|
||||||
@@ -772,7 +808,7 @@ public sealed class ConnectDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for saved session
|
// Check for saved session
|
||||||
var saved = _savedServers.FirstOrDefault(s =>
|
SavedServer? saved = _savedServers.FirstOrDefault(s =>
|
||||||
string.Equals(s.Url, url, StringComparison.OrdinalIgnoreCase) &&
|
string.Equals(s.Url, url, StringComparison.OrdinalIgnoreCase) &&
|
||||||
string.Equals(s.Username, user, StringComparison.OrdinalIgnoreCase) &&
|
string.Equals(s.Username, user, StringComparison.OrdinalIgnoreCase) &&
|
||||||
!string.IsNullOrEmpty(s.RefreshToken));
|
!string.IsNullOrEmpty(s.RefreshToken));
|
||||||
@@ -807,14 +843,14 @@ public sealed class ConnectDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
dialog.Close();
|
dialog.Close();
|
||||||
tcs.TrySetResult(result);
|
_ = tcs.TrySetResult(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerBtn.Click += (_, _) =>
|
registerBtn.Click += (_, _) =>
|
||||||
{
|
{
|
||||||
var url = urlBox.Text?.Trim() ?? "";
|
string url = urlBox.Text?.Trim() ?? "";
|
||||||
var user = userBox.Text?.Trim() ?? "";
|
string user = userBox.Text?.Trim() ?? "";
|
||||||
var pass = passBox.Text ?? "";
|
string pass = passBox.Text ?? "";
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass))
|
if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass))
|
||||||
{
|
{
|
||||||
@@ -832,28 +868,32 @@ public sealed class ConnectDialog
|
|||||||
};
|
};
|
||||||
|
|
||||||
dialog.Close();
|
dialog.Close();
|
||||||
tcs.TrySetResult(result);
|
_ = tcs.TrySetResult(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
cancelBtn.Click += (_, _) =>
|
cancelBtn.Click += (_, _) =>
|
||||||
{
|
{
|
||||||
dialog.Close();
|
dialog.Close();
|
||||||
tcs.TrySetResult(null);
|
_ = tcs.TrySetResult(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.Closed += (_, _) => tcs.TrySetResult(result);
|
dialog.Closed += (_, _) => tcs.TrySetResult(result);
|
||||||
|
|
||||||
if (owner is not null)
|
if (owner is not null)
|
||||||
|
{
|
||||||
await dialog.ShowDialog(owner);
|
await dialog.ShowDialog(owner);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
dialog.Show();
|
dialog.Show();
|
||||||
|
}
|
||||||
|
|
||||||
return await tcs.Task;
|
return await tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task ShowMessageBox(Window owner, string title, string message)
|
private static async Task ShowMessageBox(Window owner, string title, string message)
|
||||||
{
|
{
|
||||||
var msgBox = new Window
|
Window msgBox = new Window
|
||||||
{
|
{
|
||||||
Title = title,
|
Title = title,
|
||||||
Width = 350,
|
Width = 350,
|
||||||
@@ -862,14 +902,14 @@ public sealed class ConnectDialog
|
|||||||
CanResize = false,
|
CanResize = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
var stack = new StackPanel { Spacing = 10, Margin = new Avalonia.Thickness(15) };
|
StackPanel stack = new StackPanel { Spacing = 10, Margin = new Avalonia.Thickness(15) };
|
||||||
stack.Children.Add(new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap });
|
stack.Children.Add(new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap });
|
||||||
var okBtn = new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
|
Button okBtn = new Button { Content = "OK", HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
|
||||||
stack.Children.Add(okBtn);
|
stack.Children.Add(okBtn);
|
||||||
msgBox.Content = stack;
|
msgBox.Content = stack;
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
TaskCompletionSource tcs = new TaskCompletionSource();
|
||||||
okBtn.Click += (_, _) => { msgBox.Close(); tcs.TrySetResult(); };
|
okBtn.Click += (_, _) => { msgBox.Close(); _ = tcs.TrySetResult(); };
|
||||||
msgBox.Closed += (_, _) => tcs.TrySetResult();
|
msgBox.Closed += (_, _) => tcs.TrySetResult();
|
||||||
|
|
||||||
await msgBox.ShowDialog(owner);
|
await msgBox.ShowDialog(owner);
|
||||||
|
|||||||
@@ -1,50 +1,48 @@
|
|||||||
using System;
|
|
||||||
using System.Reactive;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using EchoHub.Client.Commands;
|
using EchoHub.Client.Commands;
|
||||||
using ReactiveUI;
|
|
||||||
|
using System.Reactive;
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
public sealed class MessageComposerViewModel : ViewModelBase
|
public sealed class MessageComposerViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private string _draft = string.Empty;
|
public event Action<string, string>? SendRequested;
|
||||||
private CommandHandler? _commandHandler;
|
|
||||||
private string _serverUrl = string.Empty;
|
|
||||||
|
|
||||||
public MessageComposerViewModel()
|
public event Func<string, Task<string?>>? CommandRequested;
|
||||||
{
|
|
||||||
var canSend = this.WhenAnyValue(x => x.Draft, draft => !string.IsNullOrWhiteSpace(draft));
|
public event Action<string, string>? FileUploadRequested;
|
||||||
SendCommand = ReactiveCommand.Create(Send, canSend);
|
|
||||||
}
|
private CommandHandler? _commandHandler;
|
||||||
|
|
||||||
public string Draft
|
public string Draft
|
||||||
{
|
{
|
||||||
get => _draft;
|
get;
|
||||||
set => this.RaiseAndSetIfChanged(ref _draft, value);
|
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||||
}
|
} = string.Empty;
|
||||||
|
|
||||||
public string ServerUrl => _serverUrl;
|
public string ServerUrl { get; private set; } = string.Empty;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
public event Action<string, string>? SendRequested;
|
public MessageComposerViewModel()
|
||||||
public event Func<string, Task<string?>>? CommandRequested;
|
{
|
||||||
public event Action<string, string>? FileUploadRequested;
|
IObservable<bool> canSend = this.WhenAnyValue(x => x.Draft, draft => !string.IsNullOrWhiteSpace(draft));
|
||||||
|
SendCommand = ReactiveCommand.Create(Send, canSend);
|
||||||
|
}
|
||||||
|
|
||||||
public void RequestFileUpload(string filePath)
|
public void RequestFileUpload(string filePath)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_serverUrl))
|
if (!string.IsNullOrEmpty(ServerUrl))
|
||||||
FileUploadRequested?.Invoke(_serverUrl, filePath);
|
{
|
||||||
|
FileUploadRequested?.Invoke(ServerUrl, filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetServer(string serverUrl)
|
public void SetServer(string serverUrl)
|
||||||
{
|
{
|
||||||
_serverUrl = serverUrl;
|
ServerUrl = serverUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCommandHandler(CommandHandler handler)
|
public void SetCommandHandler(CommandHandler handler)
|
||||||
@@ -53,23 +51,28 @@ public sealed class MessageComposerViewModel : ViewModelBase
|
|||||||
this.RaisePropertyChanged(nameof(HasCommandHandler));
|
this.RaisePropertyChanged(nameof(HasCommandHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsCommand(string input) => _commandHandler?.IsCommand(input) ?? input.StartsWith('/');
|
public bool IsCommand(string input)
|
||||||
|
{
|
||||||
|
return _commandHandler?.IsCommand(input) ?? input.StartsWith('/');
|
||||||
|
}
|
||||||
|
|
||||||
private void Send()
|
private void Send()
|
||||||
{
|
{
|
||||||
var text = Draft.Trim();
|
string text = Draft.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Draft = string.Empty;
|
Draft = string.Empty;
|
||||||
|
|
||||||
if (_commandHandler is not null && _commandHandler.IsCommand(text))
|
if (_commandHandler is not null && _commandHandler.IsCommand(text))
|
||||||
{
|
{
|
||||||
CommandRequested?.Invoke(text);
|
_ = (CommandRequested?.Invoke(text));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SendRequested?.Invoke(_serverUrl, text);
|
SendRequested?.Invoke(ServerUrl, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
using System;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using EchoHub.Core.Models;
|
|
||||||
using Decho.Models;
|
using Decho.Models;
|
||||||
|
|
||||||
|
using EchoHub.Core.Models;
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
public sealed class MessageViewModel : ViewModelBase
|
public sealed class MessageViewModel(MessageModel model) : ViewModelBase
|
||||||
{
|
{
|
||||||
public MessageViewModel(MessageModel model)
|
public MessageModel Model { get; } = model;
|
||||||
{
|
|
||||||
Model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageModel Model { get; }
|
|
||||||
|
|
||||||
public string AuthorName => Model.Author.DisplayName;
|
public string AuthorName => Model.Author.DisplayName;
|
||||||
|
|
||||||
@@ -20,8 +16,12 @@ public sealed class MessageViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var color = Model.Author.NicknameColor;
|
string? color = Model.Author.NicknameColor;
|
||||||
if (string.IsNullOrEmpty(color)) return null;
|
if (string.IsNullOrEmpty(color))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try { return new SolidColorBrush(Avalonia.Media.Color.Parse(color)); }
|
try { return new SolidColorBrush(Avalonia.Media.Color.Parse(color)); }
|
||||||
catch { return null; }
|
catch { return null; }
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ public sealed class MessageViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.Now;
|
DateTimeOffset now = DateTimeOffset.Now;
|
||||||
|
|
||||||
if (Model.SentAt.Date == now.Date)
|
if (Model.SentAt.Date == now.Date)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,20 +1,117 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reactive;
|
|
||||||
|
|
||||||
using Decho.Models;
|
using Decho.Models;
|
||||||
using ReactiveUI;
|
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Reactive;
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
public sealed class ServerViewModel : ViewModelBase
|
public sealed class ServerViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private bool _isExpanded = true;
|
public event Func<Task>? ConnectRequested
|
||||||
private ChannelViewModel? _selectedChannel;
|
{
|
||||||
|
add => _connectRequested = (Func<Task>?)Delegate.Combine(_connectRequested, value);
|
||||||
|
remove => _connectRequested = (Func<Task>?)Delegate.Remove(_connectRequested, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Func<Task>? DisconnectRequested
|
||||||
|
{
|
||||||
|
add => _disconnectRequested = (Func<Task>?)Delegate.Combine(_disconnectRequested, value);
|
||||||
|
remove => _disconnectRequested = (Func<Task>?)Delegate.Remove(_disconnectRequested, value);
|
||||||
|
}
|
||||||
|
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
private bool _isConnecting;
|
private bool _isConnecting;
|
||||||
private string? _connectedUser;
|
private string? _connectedUser;
|
||||||
|
|
||||||
|
private Func<Task>? _connectRequested;
|
||||||
|
|
||||||
|
private Func<Task>? _disconnectRequested;
|
||||||
|
|
||||||
|
public ServerModel Model { get; }
|
||||||
|
|
||||||
|
public string Name => Model.Name;
|
||||||
|
|
||||||
|
public string ServerUrl => Model.ServerUrl;
|
||||||
|
|
||||||
|
public ObservableCollection<ChannelViewModel> Channels { get; }
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> ConnectCommand { get; }
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> DisconnectCommand { get; }
|
||||||
|
|
||||||
|
public ChannelViewModel? SelectedChannel
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExpanded
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||||
|
} = true;
|
||||||
|
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
|
get => _isConnected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_ = this.RaiseAndSetIfChanged(ref _isConnected, value);
|
||||||
|
this.RaisePropertyChanged(nameof(ConnectionStatusText));
|
||||||
|
this.RaisePropertyChanged(nameof(ConnectionStatusColor));
|
||||||
|
this.RaisePropertyChanged(nameof(ShowConnectionControls));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsConnecting
|
||||||
|
{
|
||||||
|
get => _isConnecting;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_ = this.RaiseAndSetIfChanged(ref _isConnecting, value);
|
||||||
|
this.RaisePropertyChanged(nameof(ConnectionStatusText));
|
||||||
|
this.RaisePropertyChanged(nameof(ShowConnectionControls));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ConnectedUser
|
||||||
|
{
|
||||||
|
get => _connectedUser;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _connectedUser, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConnectionStatusText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsConnected)
|
||||||
|
{
|
||||||
|
return IsConnecting ? "Connecting..." : $"Connected as {ConnectedUser}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return IsConnecting ? "Connecting..." : "Disconnected";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConnectionStatusColor
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsConnecting)
|
||||||
|
{
|
||||||
|
return IsConnected ? "Green" : "Orange";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return IsConnected ? "Green" : "Gray";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowConnectionControls => !IsConnected && !IsConnecting;
|
||||||
|
|
||||||
public ServerViewModel(ServerModel model)
|
public ServerViewModel(ServerModel model)
|
||||||
{
|
{
|
||||||
Model = model;
|
Model = model;
|
||||||
@@ -29,91 +126,6 @@ public sealed class ServerViewModel : ViewModelBase
|
|||||||
DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync);
|
DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Func<Task>? ConnectRequested
|
|
||||||
{
|
|
||||||
add => _connectRequested = (Func<Task>?)Delegate.Combine(_connectRequested, value);
|
|
||||||
remove => _connectRequested = (Func<Task>?)Delegate.Remove(_connectRequested, value);
|
|
||||||
}
|
|
||||||
private Func<Task>? _connectRequested;
|
|
||||||
|
|
||||||
public event Func<Task>? DisconnectRequested
|
|
||||||
{
|
|
||||||
add => _disconnectRequested = (Func<Task>?)Delegate.Combine(_disconnectRequested, value);
|
|
||||||
remove => _disconnectRequested = (Func<Task>?)Delegate.Remove(_disconnectRequested, value);
|
|
||||||
}
|
|
||||||
private Func<Task>? _disconnectRequested;
|
|
||||||
|
|
||||||
public ServerModel Model { get; }
|
|
||||||
|
|
||||||
public string Name => Model.Name;
|
|
||||||
|
|
||||||
public string ServerUrl => Model.ServerUrl;
|
|
||||||
|
|
||||||
public ObservableCollection<ChannelViewModel> Channels { get; }
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> ConnectCommand { get; }
|
|
||||||
public ReactiveCommand<Unit, Unit> DisconnectCommand { get; }
|
|
||||||
|
|
||||||
public ChannelViewModel? SelectedChannel
|
|
||||||
{
|
|
||||||
get => _selectedChannel;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _selectedChannel, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsExpanded
|
|
||||||
{
|
|
||||||
get => _isExpanded;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsConnected
|
|
||||||
{
|
|
||||||
get => _isConnected;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _isConnected, value);
|
|
||||||
this.RaisePropertyChanged(nameof(ConnectionStatusText));
|
|
||||||
this.RaisePropertyChanged(nameof(ConnectionStatusColor));
|
|
||||||
this.RaisePropertyChanged(nameof(ShowConnectionControls));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsConnecting
|
|
||||||
{
|
|
||||||
get => _isConnecting;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _isConnecting, value);
|
|
||||||
this.RaisePropertyChanged(nameof(ConnectionStatusText));
|
|
||||||
this.RaisePropertyChanged(nameof(ShowConnectionControls));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? ConnectedUser
|
|
||||||
{
|
|
||||||
get => _connectedUser;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _connectedUser, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ConnectionStatusText => IsConnecting ? "Connecting..." :
|
|
||||||
IsConnected ? $"Connected as {ConnectedUser}" : "Disconnected";
|
|
||||||
|
|
||||||
public string ConnectionStatusColor => IsConnected ? "Green" : IsConnecting ? "Orange" : "Gray";
|
|
||||||
|
|
||||||
public bool ShowConnectionControls => !IsConnected && !IsConnecting;
|
|
||||||
|
|
||||||
private async Task ConnectAsync()
|
|
||||||
{
|
|
||||||
if (_connectRequested is not null)
|
|
||||||
await _connectRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DisconnectAsync()
|
|
||||||
{
|
|
||||||
if (_disconnectRequested is not null)
|
|
||||||
await _disconnectRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SyncFromModel()
|
public void SyncFromModel()
|
||||||
{
|
{
|
||||||
IsConnected = Model.IsConnected;
|
IsConnected = Model.IsConnected;
|
||||||
@@ -121,4 +133,20 @@ public sealed class ServerViewModel : ViewModelBase
|
|||||||
ConnectedUser = Model.ConnectedUser;
|
ConnectedUser = Model.ConnectedUser;
|
||||||
IsExpanded = true;
|
IsExpanded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ConnectAsync()
|
||||||
|
{
|
||||||
|
if (_connectRequested is not null)
|
||||||
|
{
|
||||||
|
await _connectRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DisconnectAsync()
|
||||||
|
{
|
||||||
|
if (_disconnectRequested is not null)
|
||||||
|
{
|
||||||
|
await _disconnectRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,44 +1,37 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
|
||||||
using Decho.Models;
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
public sealed class SidebarViewModel : ViewModelBase
|
public sealed class SidebarViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private ChannelViewModel? _selectedChannel;
|
|
||||||
|
|
||||||
public SidebarViewModel()
|
|
||||||
{
|
|
||||||
Servers = new ObservableCollection<ServerViewModel>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ObservableCollection<ServerViewModel> Servers { get; }
|
public ObservableCollection<ServerViewModel> Servers { get; }
|
||||||
|
|
||||||
public ChannelViewModel? SelectedChannel
|
public ChannelViewModel? SelectedChannel
|
||||||
{
|
{
|
||||||
get => _selectedChannel;
|
get;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(_selectedChannel, value))
|
if (ReferenceEquals(field, value))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.RaiseAndSetIfChanged(ref _selectedChannel, value);
|
_ = this.RaiseAndSetIfChanged(ref field, value);
|
||||||
|
|
||||||
if (value is null)
|
if (value is null)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var server in Servers)
|
foreach (ServerViewModel server in Servers)
|
||||||
{
|
{
|
||||||
if (server.Channels.Contains(value))
|
if (server.Channels.Contains(value))
|
||||||
{
|
{
|
||||||
if (!ReferenceEquals(server.SelectedChannel, value))
|
if (!ReferenceEquals(server.SelectedChannel, value))
|
||||||
|
{
|
||||||
server.SelectedChannel = value;
|
server.SelectedChannel = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (server.SelectedChannel is not null)
|
else if (server.SelectedChannel is not null)
|
||||||
{
|
{
|
||||||
@@ -48,26 +41,23 @@ public sealed class SidebarViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddServer(ServerModel model)
|
public SidebarViewModel()
|
||||||
{
|
{
|
||||||
var vm = new ServerViewModel(model);
|
Servers = [];
|
||||||
vm.WhenAnyValue(s => s.SelectedChannel)
|
|
||||||
.Where(channel => channel is not null)
|
|
||||||
.Subscribe(channel => SelectedChannel = channel!);
|
|
||||||
|
|
||||||
Servers.Add(vm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveServer(string serverUrl)
|
public void RemoveServer(string serverUrl)
|
||||||
{
|
{
|
||||||
var server = Servers.FirstOrDefault(s =>
|
ServerViewModel? server = Servers.FirstOrDefault(s =>
|
||||||
string.Equals(s.ServerUrl, serverUrl, StringComparison.OrdinalIgnoreCase));
|
string.Equals(s.ServerUrl, serverUrl, StringComparison.OrdinalIgnoreCase));
|
||||||
if (server is not null)
|
if (server is not null)
|
||||||
{
|
{
|
||||||
if (SelectedChannel is not null && server.Channels.Contains(SelectedChannel))
|
if (SelectedChannel is not null && server.Channels.Contains(SelectedChannel))
|
||||||
|
{
|
||||||
SelectedChannel = null;
|
SelectedChannel = null;
|
||||||
|
}
|
||||||
|
|
||||||
Servers.Remove(server);
|
_ = Servers.Remove(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using ReactiveUI;
|
namespace Decho.ViewModels;
|
||||||
|
|
||||||
namespace Decho.ViewModels;
|
|
||||||
|
|
||||||
public abstract class ViewModelBase : ReactiveObject
|
public abstract class ViewModelBase : ReactiveObject
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,4 @@ public partial class ChannelListView : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,4 @@ public partial class ChatView : UserControl
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Linq;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
using Decho.ViewModels;
|
using Decho.ViewModels;
|
||||||
|
|
||||||
namespace Decho.Views;
|
namespace Decho.Views;
|
||||||
@@ -19,18 +19,24 @@ public partial class MessageComposerView : UserControl
|
|||||||
|
|
||||||
private async void OnFileUploadClicked(object? sender, RoutedEventArgs e)
|
private async void OnFileUploadClicked(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not MessageComposerViewModel vm) return;
|
if (DataContext is not MessageComposerViewModel vm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
TopLevel? topLevel = TopLevel.GetTopLevel(this);
|
||||||
if (topLevel?.StorageProvider is null) return;
|
if (topLevel?.StorageProvider is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
IReadOnlyList<IStorageFile> files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
{
|
{
|
||||||
AllowMultiple = false,
|
AllowMultiple = false,
|
||||||
Title = "Select a file to upload",
|
Title = "Select a file to upload",
|
||||||
});
|
});
|
||||||
|
|
||||||
var file = files?.FirstOrDefault();
|
IStorageFile? file = files?.FirstOrDefault();
|
||||||
if (file?.TryGetLocalPath() is string path)
|
if (file?.TryGetLocalPath() is string path)
|
||||||
{
|
{
|
||||||
vm.RequestFileUpload(path);
|
vm.RequestFileUpload(path);
|
||||||
@@ -41,21 +47,28 @@ public partial class MessageComposerView : UserControl
|
|||||||
{
|
{
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
if (e.Data.Contains(DataFormats.Files))
|
if (e.Data.Contains(DataFormats.Files))
|
||||||
|
{
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
e.DragEffects = DragDropEffects.Copy;
|
e.DragEffects = DragDropEffects.Copy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDrop(object? sender, DragEventArgs e)
|
private void OnDrop(object? sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not MessageComposerViewModel vm) return;
|
if (DataContext is not MessageComposerViewModel vm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
var paths = e.Data.GetFiles()?
|
string? paths = e.Data.GetFiles()?
|
||||||
.Select(f => f.TryGetLocalPath())
|
.Select(f => f.TryGetLocalPath())
|
||||||
.FirstOrDefault(p => p is not null);
|
.FirstOrDefault(p => p is not null);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
if (paths is string path)
|
if (paths is string path)
|
||||||
|
{
|
||||||
vm.RequestFileUpload(path);
|
vm.RequestFileUpload(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
using Decho.ViewModels;
|
using Decho.ViewModels;
|
||||||
|
|
||||||
namespace Decho.Views;
|
namespace Decho.Views;
|
||||||
@@ -30,9 +28,13 @@ public partial class MessageItemView : UserControl
|
|||||||
|
|
||||||
private void OnLoaded(object? sender, RoutedEventArgs e)
|
private void OnLoaded(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!_pendingLoad) return;
|
if (!_pendingLoad)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_pendingLoad = false;
|
_pendingLoad = false;
|
||||||
var msg = (MessageViewModel)DataContext!;
|
MessageViewModel msg = (MessageViewModel)DataContext!;
|
||||||
_ = LoadImageAsync(msg, _loadCts!.Token);
|
_ = LoadImageAsync(msg, _loadCts!.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,21 +42,26 @@ public partial class MessageItemView : UserControl
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
TopLevel? topLevel = TopLevel.GetTopLevel(this);
|
||||||
if (topLevel?.DataContext is not MainWindowViewModel mainVm) return;
|
if (topLevel?.DataContext is not MainWindowViewModel mainVm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
|
byte[]? bytes = await mainVm.ConnectionService.DownloadImageBytesAsync(
|
||||||
msg.ServerUrl ?? "", msg.AttachmentUrl!);
|
msg.ServerUrl ?? "", msg.AttachmentUrl!);
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
if (bytes is null || bytes.Length == 0) return;
|
if (bytes is null || bytes.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream(bytes);
|
using MemoryStream stream = new MemoryStream(bytes);
|
||||||
var bitmap = new Bitmap(stream);
|
Bitmap bitmap = new Bitmap(stream);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
var image = this.FindControl<Image>("MessageImage");
|
Image? image = this.FindControl<Image>("MessageImage");
|
||||||
if (image is not null)
|
_ = image?.Source = bitmap;
|
||||||
image.Source = bitmap;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -64,21 +71,35 @@ public partial class MessageItemView : UserControl
|
|||||||
|
|
||||||
private async void OnDownloadClicked(object? sender, RoutedEventArgs e)
|
private async void OnDownloadClicked(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not MessageViewModel msg) return;
|
if (DataContext is not MessageViewModel msg)
|
||||||
if (msg.AttachmentUrl is null) return;
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
if (msg.AttachmentUrl is null)
|
||||||
if (topLevel?.StorageProvider is null) return;
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var mainVm = topLevel.DataContext as MainWindowViewModel;
|
TopLevel? topLevel = TopLevel.GetTopLevel(this);
|
||||||
if (mainVm is null) return;
|
if (topLevel?.StorageProvider is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var tempPath = await mainVm.ConnectionService.DownloadAttachmentAsync(
|
if (topLevel.DataContext is not MainWindowViewModel mainVm)
|
||||||
msg.ServerUrl ?? "", msg.AttachmentUrl, msg.AttachmentFileName ?? "download");
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tempPath is null) return;
|
string? tempPath = await mainVm.ConnectionService.DownloadAttachmentAsync(msg.ServerUrl ?? "", msg.AttachmentUrl, msg.AttachmentFileName ?? "download");
|
||||||
|
|
||||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
if (tempPath is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||||
{
|
{
|
||||||
SuggestedFileName = msg.AttachmentFileName ?? "download",
|
SuggestedFileName = msg.AttachmentFileName ?? "download",
|
||||||
});
|
});
|
||||||
@@ -88,7 +109,13 @@ public partial class MessageItemView : UserControl
|
|||||||
File.Copy(tempPath, savePath, overwrite: true);
|
File.Copy(tempPath, savePath, overwrite: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
try { File.Delete(tempPath); }
|
try
|
||||||
catch { }
|
{
|
||||||
|
File.Delete(tempPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore if temp file cannot be deleted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Collections.Specialized;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
using Decho.ViewModels;
|
using Decho.ViewModels;
|
||||||
|
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
|
||||||
namespace Decho.Views;
|
namespace Decho.Views;
|
||||||
|
|
||||||
public partial class MessageListView : UserControl
|
public partial class MessageListView : UserControl
|
||||||
@@ -25,17 +27,15 @@ public partial class MessageListView : UserControl
|
|||||||
_chat.PropertyChanged -= OnViewModelPropertyChanged;
|
_chat.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scroll = this.FindControl<ScrollViewer>("MessageScrollViewer");
|
ScrollViewer? scroll = this.FindControl<ScrollViewer>("MessageScrollViewer");
|
||||||
if (scroll is not null)
|
scroll?.ScrollChanged -= OnScrollChanged;
|
||||||
scroll.ScrollChanged -= OnScrollChanged;
|
|
||||||
|
|
||||||
_chat = DataContext as ChatViewModel;
|
_chat = DataContext as ChatViewModel;
|
||||||
if (_chat is not null)
|
if (_chat is not null)
|
||||||
{
|
{
|
||||||
_chat.Messages.CollectionChanged += OnMessagesChanged;
|
_chat.Messages.CollectionChanged += OnMessagesChanged;
|
||||||
_chat.PropertyChanged += OnViewModelPropertyChanged;
|
_chat.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
if (scroll is not null)
|
scroll?.ScrollChanged += OnScrollChanged;
|
||||||
scroll.ScrollChanged += OnScrollChanged;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +43,11 @@ public partial class MessageListView : UserControl
|
|||||||
{
|
{
|
||||||
if (e.ExtentDelta.Y > 0 && _wasAtBottom)
|
if (e.ExtentDelta.Y > 0 && _wasAtBottom)
|
||||||
{
|
{
|
||||||
var scroll = (ScrollViewer)sender!;
|
ScrollViewer scroll = (ScrollViewer)sender!;
|
||||||
Dispatcher.UIThread.Post(scroll.ScrollToEnd, DispatcherPriority.Background);
|
Dispatcher.UIThread.Post(scroll.ScrollToEnd, DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scrollViewer = (ScrollViewer)sender!;
|
ScrollViewer scrollViewer = (ScrollViewer)sender!;
|
||||||
_wasAtBottom = scrollViewer.Offset.Y + scrollViewer.Viewport.Height >= scrollViewer.Extent.Height - 30;
|
_wasAtBottom = scrollViewer.Offset.Y + scrollViewer.Viewport.Height >= scrollViewer.Extent.Height - 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +70,11 @@ public partial class MessageListView : UserControl
|
|||||||
|
|
||||||
private void TryAutoScroll()
|
private void TryAutoScroll()
|
||||||
{
|
{
|
||||||
var scroll = this.FindControl<ScrollViewer>("MessageScrollViewer");
|
ScrollViewer? scroll = this.FindControl<ScrollViewer>("MessageScrollViewer");
|
||||||
if (scroll is null) return;
|
if (scroll is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_wasAtBottom = scroll.Offset.Y + scroll.Viewport.Height >= scroll.Extent.Height - 30;
|
_wasAtBottom = scroll.Offset.Y + scroll.Viewport.Height >= scroll.Extent.Height - 30;
|
||||||
if (_wasAtBottom && !_hasPendingScroll)
|
if (_wasAtBottom && !_hasPendingScroll)
|
||||||
@@ -87,10 +90,13 @@ public partial class MessageListView : UserControl
|
|||||||
|
|
||||||
private void ScrollToBottom()
|
private void ScrollToBottom()
|
||||||
{
|
{
|
||||||
var scroll = this.FindControl<ScrollViewer>("MessageScrollViewer");
|
ScrollViewer? scroll = this.FindControl<ScrollViewer>("MessageScrollViewer");
|
||||||
if (scroll is null) return;
|
if (scroll is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_wasAtBottom = true;
|
_wasAtBottom = true;
|
||||||
Dispatcher.UIThread.Post(scroll.ScrollToEnd, DispatcherPriority.Background);
|
Dispatcher.UIThread.Post(scroll.ScrollToEnd, DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,14 +12,14 @@ public partial class SidebarView : UserControl
|
|||||||
nameof(SelectedChannel),
|
nameof(SelectedChannel),
|
||||||
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
|
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
|
||||||
|
|
||||||
public SidebarView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelViewModel? SelectedChannel
|
public ChannelViewModel? SelectedChannel
|
||||||
{
|
{
|
||||||
get => GetValue(SelectedChannelProperty);
|
get => GetValue(SelectedChannelProperty);
|
||||||
set => SetValue(SelectedChannelProperty, value);
|
set => SetValue(SelectedChannelProperty, value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public SidebarView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user