Add unread and mention badges with notification sounds for channels

This commit is contained in:
Stone_Red
2026-07-16 14:46:05 +02:00
parent 4b0a06db4e
commit 9ceff54de6
5 changed files with 106 additions and 11 deletions
+1
View File
@@ -24,6 +24,7 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" /> <PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
<PackageReference Include="NetCoreAudio" Version="2.0.1" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" /> <PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" /> <PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
<PackageReference Include="Romzetron.Avalonia" Version="11.3.4" /> <PackageReference Include="Romzetron.Avalonia" Version="11.3.4" />
+59
View File
@@ -36,9 +36,68 @@ public sealed class ChannelViewModel(ChannelModel model) : ViewModelBase
set => this.RaiseAndSetIfChanged(ref field, value); set => this.RaiseAndSetIfChanged(ref field, value);
} }
public int UnreadCount
{
get => field;
set
{
if (field == value)
{
return;
}
field = value;
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(HasUnread));
this.RaisePropertyChanged(nameof(BadgeText));
this.RaisePropertyChanged(nameof(BadgeColor));
}
}
public bool HasUnread => UnreadCount > 0;
public int MentionCount
{
get => field;
set
{
if (field == value)
{
return;
}
field = value;
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(HasMentions));
this.RaisePropertyChanged(nameof(BadgeText));
this.RaisePropertyChanged(nameof(BadgeColor));
}
}
public bool HasMentions => MentionCount > 0;
public string BadgeColor => HasMentions ? "#E53935" : "#78909C";
public string BadgeText => HasMentions ? MentionCount.ToString() : UnreadCount.ToString();
public ObservableCollection<MessageViewModel> Messages { get; } = new ObservableCollection<MessageViewModel>( public ObservableCollection<MessageViewModel> Messages { get; } = new ObservableCollection<MessageViewModel>(
model.Messages.Select(message => new MessageViewModel(message))); model.Messages.Select(message => new MessageViewModel(message)));
public void IncrementUnread(bool isMention = false)
{
UnreadCount++;
if (isMention)
{
MentionCount++;
}
}
public void ClearUnread()
{
UnreadCount = 0;
MentionCount = 0;
}
public void ClearMessages() public void ClearMessages()
{ {
Model.Messages.Clear(); Model.Messages.Clear();
+25 -2
View File
@@ -6,6 +6,7 @@ using Decho.Views;
using EchoHub.Client.Commands; using EchoHub.Client.Commands;
using EchoHub.Client.Config; using EchoHub.Client.Config;
using EchoHub.Client.Services;
using EchoHub.Core.Constants; using EchoHub.Core.Constants;
using EchoHub.Core.DTOs; using EchoHub.Core.DTOs;
using EchoHub.Core.Models; using EchoHub.Core.Models;
@@ -22,6 +23,7 @@ namespace Decho.ViewModels;
public sealed class MainWindowViewModel : ViewModelBase public sealed class MainWindowViewModel : ViewModelBase
{ {
private readonly CommandHandler _commandHandler; private readonly CommandHandler _commandHandler;
private readonly NotificationSoundService _notificationService;
private Window? _mainWindow; private Window? _mainWindow;
public string Title { get; } = "Decho"; public string Title { get; } = "Decho";
@@ -44,6 +46,7 @@ public sealed class MainWindowViewModel : ViewModelBase
{ {
ConnectionService = new ConnectionService(); ConnectionService = new ConnectionService();
_commandHandler = new CommandHandler(); _commandHandler = new CommandHandler();
_notificationService = new NotificationSoundService(ConfigManager.Load().Notifications);
Sidebar = new SidebarViewModel(); Sidebar = new SidebarViewModel();
Chat = new ChatViewModel(); Chat = new ChatViewModel();
@@ -360,7 +363,7 @@ public sealed class MainWindowViewModel : ViewModelBase
await ConnectionService.NukeChannelAsync(serverUrl, channel); await ConnectionService.NukeChannelAsync(serverUrl, channel);
}; };
_commandHandler.OnTestSound += () => Task.CompletedTask; _commandHandler.OnTestSound += _notificationService.PlayTestAsync;
_commandHandler.OnQuit += () => _commandHandler.OnQuit += () =>
{ {
@@ -554,11 +557,29 @@ public sealed class MainWindowViewModel : ViewModelBase
ConnectionService.MessageReceived += (serverUrl, message) => ConnectionService.MessageReceived += (serverUrl, message) =>
{ {
string? username = ConnectionService.GetCurrentUsername(serverUrl);
bool isMention = !string.IsNullOrEmpty(username)
&& message.Content.Contains($"@{username}", StringComparison.OrdinalIgnoreCase);
Avalonia.Threading.Dispatcher.UIThread.Post(() => Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{ {
ChannelViewModel? channelVm = FindChannelViewModel(serverUrl, message.ChannelName); ChannelViewModel? channelVm = FindChannelViewModel(serverUrl, message.ChannelName);
channelVm?.AddMessage(message); if (channelVm is not null)
{
channelVm.AddMessage(message);
bool isSelected = string.Equals(Chat.CurrentChannelName, message.ChannelName, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Chat.CurrentServerUrl, serverUrl, StringComparison.OrdinalIgnoreCase);
if (!isSelected)
{
channelVm.IncrementUnread(isMention);
}
}
}); });
if (isMention)
{
_ = _notificationService.PlayAsync();
}
}; };
ConnectionService.ErrorOccurred += (serverUrl, error) => ConnectionService.ErrorOccurred += (serverUrl, error) =>
@@ -646,6 +667,8 @@ public sealed class MainWindowViewModel : ViewModelBase
return; return;
} }
channel.ClearUnread();
string serverUrl = FindServerUrlForChannel(channel); string serverUrl = FindServerUrlForChannel(channel);
bool isServerConnected = Sidebar.GetServer(serverUrl)?.IsConnected ?? false; bool isServerConnected = Sidebar.GetServer(serverUrl)?.IsConnected ?? false;
Chat.SetChannel(channel, serverUrl, isServerConnected); Chat.SetChannel(channel, serverUrl, isServerConnected);
+3 -3
View File
@@ -25,12 +25,12 @@ public sealed class MessageViewModel(MessageModel model) : ViewModelBase
return Brushes.White; return Brushes.White;
} }
try try
{ {
return new SolidColorBrush(Color.Parse(color)); return new SolidColorBrush(Color.Parse(color));
} }
catch catch
{ {
return null; return null;
} }
} }
+18 -6
View File
@@ -12,12 +12,24 @@
SelectedItem="{Binding SelectedChannel, Mode=TwoWay}"> SelectedItem="{Binding SelectedChannel, Mode=TwoWay}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ChannelViewModel"> <DataTemplate x:DataType="vm:ChannelViewModel">
<TextBlock> <Grid ColumnDefinitions="Auto, *, Auto" Margin="2 0">
<InlineUIContainer BaselineAlignment="Bottom"> <i:Icon Grid.Column="0" Value="fa-hashtag" FontWeight="Bold" Margin="0 0 4 1" VerticalAlignment="Center" />
<i:Icon Value="fa-hashtag" FontWeight="Bold" Margin="0 0 0 1" /> <TextBlock Grid.Column="1" Text="{Binding Name}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
</InlineUIContainer> <Border Grid.Column="2"
<Run Text="{Binding Name}" /> IsVisible="{Binding HasUnread}"
</TextBlock> Background="{Binding BadgeColor}"
CornerRadius="8"
MinWidth="16"
Height="16"
Padding="4 0"
Margin="4 0 0 0">
<TextBlock Text="{Binding BadgeText}"
FontSize="11"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>