mirror of
https://github.com/Stone-Red-Code/Decho.git
synced 2026-09-04 08:56:14 +02:00
Add online users panel
This commit is contained in:
@@ -204,9 +204,9 @@ public sealed class ConnectionService : IDisposable
|
||||
|
||||
if (password is not null)
|
||||
{
|
||||
var salt = RoomCrypto.GenerateSalt();
|
||||
var derived = RoomCrypto.DeriveKeys(password, salt);
|
||||
var roomKey = RoomCrypto.GenerateRoomKey();
|
||||
byte[] salt = RoomCrypto.GenerateSalt();
|
||||
RoomCrypto.DerivedKeys derived = RoomCrypto.DeriveKeys(password, salt);
|
||||
byte[] roomKey = RoomCrypto.GenerateRoomKey();
|
||||
wirePassword = derived.AuthKeyHex;
|
||||
saltB64 = Convert.ToBase64String(salt);
|
||||
wrappedKey = RoomCrypto.WrapRoomKey(roomKey, derived.KeyEncryptionKey);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using EchoHub.Core.DTOs;
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive;
|
||||
|
||||
namespace Decho.ViewModels;
|
||||
|
||||
@@ -10,6 +13,8 @@ public sealed class ChatViewModel : ViewModelBase
|
||||
private set => this.RaiseAndSetIfChanged(ref field, value);
|
||||
} = [];
|
||||
|
||||
public ObservableCollection<UserViewModel> OnlineUsers { get; } = [];
|
||||
|
||||
public string ChannelTitle
|
||||
{
|
||||
get;
|
||||
@@ -32,6 +37,20 @@ public sealed class ChatViewModel : ViewModelBase
|
||||
private set => this.RaiseAndSetIfChanged(ref field, value);
|
||||
}
|
||||
|
||||
public bool ShowOnlineUsers
|
||||
{
|
||||
get => field;
|
||||
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> ToggleUsersPanelCommand { get; }
|
||||
|
||||
public string OnlineUserCount
|
||||
{
|
||||
get => field;
|
||||
set => this.RaiseAndSetIfChanged(ref field, value);
|
||||
} = string.Empty;
|
||||
|
||||
public MessageComposerViewModel Composer { get; }
|
||||
|
||||
public string CurrentServerUrl { get; private set; } = string.Empty;
|
||||
@@ -41,6 +60,12 @@ public sealed class ChatViewModel : ViewModelBase
|
||||
public ChatViewModel()
|
||||
{
|
||||
Composer = new MessageComposerViewModel();
|
||||
ToggleUsersPanelCommand = ReactiveCommand.Create(ToggleUsersPanel);
|
||||
}
|
||||
|
||||
private void ToggleUsersPanel()
|
||||
{
|
||||
ShowOnlineUsers = !ShowOnlineUsers;
|
||||
}
|
||||
|
||||
public void SetChannel(ChannelViewModel? channel, string serverUrl = "", bool isServerConnected = true)
|
||||
@@ -53,6 +78,9 @@ public sealed class ChatViewModel : ViewModelBase
|
||||
HasTopic = false;
|
||||
CurrentChannelName = string.Empty;
|
||||
CurrentServerUrl = string.Empty;
|
||||
ShowOnlineUsers = false;
|
||||
OnlineUsers.Clear();
|
||||
OnlineUserCount = string.Empty;
|
||||
Composer.SetServer(string.Empty, isServerConnected);
|
||||
return;
|
||||
}
|
||||
@@ -64,6 +92,45 @@ public sealed class ChatViewModel : ViewModelBase
|
||||
CurrentChannelName = channel.Name;
|
||||
CurrentServerUrl = serverUrl;
|
||||
Composer.SetServer(serverUrl, isServerConnected);
|
||||
|
||||
if (!isServerConnected)
|
||||
{
|
||||
ShowOnlineUsers = false;
|
||||
OnlineUsers.Clear();
|
||||
OnlineUserCount = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOnlineUsers(List<UserPresenceDto> users)
|
||||
{
|
||||
OnlineUsers.Clear();
|
||||
foreach (UserPresenceDto user in users)
|
||||
{
|
||||
OnlineUsers.Add(new UserViewModel(user));
|
||||
}
|
||||
OnlineUserCount = $"{users.Count}";
|
||||
ShowOnlineUsers = true;
|
||||
}
|
||||
|
||||
public void AddOnlineUser(UserPresenceDto user)
|
||||
{
|
||||
if (OnlineUsers.All(u => u.Username != user.Username))
|
||||
{
|
||||
OnlineUsers.Add(new UserViewModel(user));
|
||||
OnlineUserCount = $"{OnlineUsers.Count}";
|
||||
}
|
||||
ShowOnlineUsers = OnlineUsers.Count > 0;
|
||||
}
|
||||
|
||||
public void RemoveOnlineUser(string username)
|
||||
{
|
||||
UserViewModel? user = OnlineUsers.FirstOrDefault(u => u.Username == username);
|
||||
if (user is not null)
|
||||
{
|
||||
_ = OnlineUsers.Remove(user);
|
||||
OnlineUserCount = $"{OnlineUsers.Count}";
|
||||
}
|
||||
ShowOnlineUsers = OnlineUsers.Count > 0;
|
||||
}
|
||||
|
||||
public void ClearMessages()
|
||||
@@ -72,5 +139,7 @@ public sealed class ChatViewModel : ViewModelBase
|
||||
ChannelTitle = "Select a channel";
|
||||
ChannelTopic = null;
|
||||
HasTopic = false;
|
||||
OnlineUsers.Clear();
|
||||
OnlineUserCount = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -584,6 +584,24 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
};
|
||||
|
||||
ConnectionService.UserJoined += (serverUrl, channelName, username) =>
|
||||
{
|
||||
if (string.Equals(channelName, Chat.CurrentChannelName, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(serverUrl, Chat.CurrentServerUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_ = RefreshOnlineUsersAsync(serverUrl, channelName);
|
||||
}
|
||||
};
|
||||
|
||||
ConnectionService.UserLeft += (serverUrl, channelName) =>
|
||||
{
|
||||
if (string.Equals(channelName, Chat.CurrentChannelName, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(serverUrl, Chat.CurrentServerUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_ = RefreshOnlineUsersAsync(serverUrl, channelName);
|
||||
}
|
||||
};
|
||||
|
||||
ConnectionService.ErrorOccurred += (serverUrl, error) =>
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
@@ -593,6 +611,22 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
};
|
||||
}
|
||||
|
||||
private async Task RefreshOnlineUsersAsync(string serverUrl, string channelName)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<UserPresenceDto> users = await ConnectionService.GetOnlineUsersAsync(serverUrl, channelName);
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Chat.SetOnlineUsers(users);
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> HandleCommandAsync(string commandText)
|
||||
{
|
||||
CommandResult result = await _commandHandler.HandleAsync(commandText);
|
||||
@@ -663,11 +697,17 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
|
||||
private async Task HandleCreateChannelRequested(ServerViewModel server)
|
||||
{
|
||||
if (_mainWindow is null) return;
|
||||
if (_mainWindow is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CreateChannelWindow dialog = new CreateChannelWindow();
|
||||
bool? result = await dialog.ShowDialog<bool?>(_mainWindow);
|
||||
if (result != true) return;
|
||||
if (result != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -716,7 +756,10 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
|
||||
private async Task HandleDeleteChannelRequested(ServerViewModel server)
|
||||
{
|
||||
if (_mainWindow is null) return;
|
||||
if (_mainWindow is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string channelName = Chat.CurrentChannelName;
|
||||
if (string.IsNullOrEmpty(channelName) || !string.Equals(Chat.CurrentServerUrl, server.ServerUrl, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -738,7 +781,10 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
$"Are you sure you want to delete #{channelName}?\nThis will remove all messages permanently.",
|
||||
ButtonEnum.YesNo);
|
||||
ButtonResult confirm = await confirmBox.ShowWindowDialogAsync(_mainWindow);
|
||||
if (confirm != ButtonResult.Yes) return;
|
||||
if (confirm != ButtonResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -814,6 +860,7 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
});
|
||||
|
||||
_ = RefreshOnlineUsersAsync(serverUrl, channel.Name);
|
||||
return;
|
||||
}
|
||||
catch (EchoHub.Client.Services.ChannelPasswordRequiredException)
|
||||
@@ -830,6 +877,12 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
|
||||
if (string.IsNullOrEmpty(pwd))
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Chat.OnlineUsers.Clear();
|
||||
Chat.ShowOnlineUsers = false;
|
||||
Chat.OnlineUserCount = string.Empty;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -837,6 +890,12 @@ public sealed class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
catch
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Chat.OnlineUsers.Clear();
|
||||
Chat.ShowOnlineUsers = false;
|
||||
Chat.OnlineUserCount = string.Empty;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using Avalonia.Media;
|
||||
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
|
||||
namespace Decho.ViewModels;
|
||||
|
||||
public sealed class UserViewModel : ViewModelBase
|
||||
{
|
||||
public string Username { get; }
|
||||
public string DisplayName { get; }
|
||||
public string? NicknameColor { get; }
|
||||
public string? StatusMessage { get; }
|
||||
public ServerRole Role { get; }
|
||||
|
||||
public UserStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (_status == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_status = value;
|
||||
this.RaisePropertyChanged();
|
||||
this.RaisePropertyChanged(nameof(StatusColor));
|
||||
this.RaisePropertyChanged(nameof(StatusText));
|
||||
}
|
||||
}
|
||||
|
||||
private UserStatus _status;
|
||||
|
||||
public string FullName => DisplayName ?? Username;
|
||||
|
||||
public IBrush? DisplayColor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(NicknameColor))
|
||||
{
|
||||
return Brushes.White;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(NicknameColor));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusColor => Status switch
|
||||
{
|
||||
UserStatus.Online => "#4CAF50",
|
||||
UserStatus.Away => "#FF9800",
|
||||
UserStatus.DoNotDisturb => "#E53935",
|
||||
UserStatus.Invisible => "#9E9E9E",
|
||||
_ => "#9E9E9E",
|
||||
};
|
||||
|
||||
public string StatusText => Status switch
|
||||
{
|
||||
UserStatus.Online => "Online",
|
||||
UserStatus.Away => "Away",
|
||||
UserStatus.DoNotDisturb => "Do Not Disturb",
|
||||
UserStatus.Invisible => "Invisible",
|
||||
_ => "Offline",
|
||||
};
|
||||
|
||||
public UserViewModel(UserPresenceDto dto)
|
||||
{
|
||||
Username = dto.Username;
|
||||
DisplayName = dto.DisplayName ?? dto.Username;
|
||||
NicknameColor = dto.NicknameColor;
|
||||
StatusMessage = dto.StatusMessage;
|
||||
Role = dto.Role;
|
||||
_status = dto.Status;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:Decho.ViewModels"
|
||||
xmlns:views="using:Decho.Views"
|
||||
x:Class="Decho.Views.ChatView"
|
||||
x:DataType="vm:ChatViewModel">
|
||||
<Grid RowDefinitions="Auto, *, Auto" Margin="10">
|
||||
<TextBlock FontWeight="Bold"
|
||||
FontSize="16"
|
||||
Margin="0 0 0 8"
|
||||
Text="{Binding ChannelTitle}" />
|
||||
<views:MessageListView Grid.Row="1" />
|
||||
<views:MessageComposerView Grid.Row="2"
|
||||
DataContext="{Binding Composer}" />
|
||||
</Grid>
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:Decho.ViewModels"
|
||||
xmlns:views="using:Decho.Views"
|
||||
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||
x:Class="Decho.Views.ChatView"
|
||||
x:DataType="vm:ChatViewModel">
|
||||
<Grid RowDefinitions="Auto, Auto, *, Auto" Margin="10">
|
||||
<Grid ColumnDefinitions="*, Auto">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock FontWeight="Bold"
|
||||
FontSize="16"
|
||||
Margin="0 0 4 4"
|
||||
Text="{Binding ChannelTitle}" />
|
||||
<TextBlock Text="{Binding ChannelTopic}"
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
Opacity="0.7" />
|
||||
</StackPanel>
|
||||
<Button i:Attached.Icon="fa-users"
|
||||
Grid.Column="1"
|
||||
Margin="-10"
|
||||
Background="Transparent"
|
||||
Foreground="{DynamicResource UiTheme08}"
|
||||
Command="{Binding ToggleUsersPanelCommand}"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
<views:MessageListView Grid.Row="2" />
|
||||
<views:MessageComposerView Grid.Row="3"
|
||||
DataContext="{Binding Composer}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -19,7 +19,7 @@ public partial class CreateChannelWindow : Window
|
||||
string name = ChannelName.Text?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
ChannelName.Focus();
|
||||
_ = ChannelName.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<vm:MainWindowViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto, *" ColumnDefinitions="Auto, *">
|
||||
<Grid RowDefinitions="Auto, *" ColumnDefinitions="Auto, *, Auto">
|
||||
<DockPanel HorizontalAlignment="Stretch">
|
||||
<TextBlock FontSize="20"
|
||||
FontWeight="Bold"
|
||||
@@ -44,5 +44,8 @@
|
||||
<views:ChatView Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
DataContext="{Binding Chat}" />
|
||||
<views:OnlineUsersView Grid.RowSpan="2"
|
||||
Grid.Column="2"
|
||||
DataContext="{Binding Chat}" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,37 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:Decho.ViewModels"
|
||||
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||
x:Class="Decho.Views.OnlineUsersView"
|
||||
x:DataType="vm:ChatViewModel">
|
||||
<Border Background="{DynamicResource UiTheme00}"
|
||||
Width="200"
|
||||
IsVisible="{Binding ShowOnlineUsers}">
|
||||
<StackPanel>
|
||||
<TextBlock FontWeight="Bold"
|
||||
FontSize="13"
|
||||
Margin="8 6"
|
||||
Text="{Binding OnlineUserCount, StringFormat='Online — {0}'}" />
|
||||
<ItemsControl ItemsSource="{Binding OnlineUsers}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:UserViewModel">
|
||||
<Grid ColumnDefinitions="Auto, *" Margin="8 6">
|
||||
<Ellipse Width="8" Height="8"
|
||||
Fill="{Binding StatusColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 6 0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding FullName}"
|
||||
Foreground="{Binding DisplayColor}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Cursor="Hand"
|
||||
PointerPressed="OnUserNamePointerPressed" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,43 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
using Decho.ViewModels;
|
||||
|
||||
using EchoHub.Core.DTOs;
|
||||
|
||||
namespace Decho.Views;
|
||||
|
||||
public partial class OnlineUsersView : UserControl
|
||||
{
|
||||
public OnlineUsersView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnUserNamePointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is not TextBlock textBlock || textBlock.DataContext is not UserViewModel user)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TopLevel.GetTopLevel(this) is not Window parent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (parent.DataContext is not MainWindowViewModel mainVm)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string serverUrl = mainVm.Chat.CurrentServerUrl;
|
||||
UserProfileDto? profile = await mainVm.ConnectionService.GetUserProfileAsync(serverUrl, user.Username);
|
||||
if (profile is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProfileWindow dialog = new ProfileWindow(profile);
|
||||
await dialog.ShowDialog(parent);
|
||||
}
|
||||
}
|
||||
@@ -49,13 +49,13 @@
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
<MenuItem Header="Remove Server"
|
||||
Command="{Binding RemoveCommand}" />
|
||||
<Separator />
|
||||
<MenuItem Header="Create Channel"
|
||||
Command="{Binding CreateChannelCommand}"
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
<MenuItem Header="Delete Channel"
|
||||
Command="{Binding DeleteChannelCommand}"
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
<Separator IsVisible="{Binding IsConnected}" />
|
||||
<MenuItem Header="Create Channel"
|
||||
Command="{Binding CreateChannelCommand}"
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
<MenuItem Header="Delete Channel"
|
||||
Command="{Binding DeleteChannelCommand}"
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user