From 9ceff54de69a8d3381f28350d00d7ee69a750dac Mon Sep 17 00:00:00 2001
From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com>
Date: Thu, 16 Jul 2026 14:46:05 +0200
Subject: [PATCH] Add unread and mention badges with notification sounds for
channels
---
src/Decho/Decho.csproj | 1 +
src/Decho/ViewModels/ChannelViewModel.cs | 59 +++++++++++++++++++++
src/Decho/ViewModels/MainWindowViewModel.cs | 27 +++++++++-
src/Decho/ViewModels/MessageViewModel.cs | 6 +--
src/Decho/Views/ChannelListView.axaml | 24 ++++++---
5 files changed, 106 insertions(+), 11 deletions(-)
diff --git a/src/Decho/Decho.csproj b/src/Decho/Decho.csproj
index f955f84..932dc1f 100644
--- a/src/Decho/Decho.csproj
+++ b/src/Decho/Decho.csproj
@@ -24,6 +24,7 @@
All
+
diff --git a/src/Decho/ViewModels/ChannelViewModel.cs b/src/Decho/ViewModels/ChannelViewModel.cs
index 387aca4..fb9a220 100644
--- a/src/Decho/ViewModels/ChannelViewModel.cs
+++ b/src/Decho/ViewModels/ChannelViewModel.cs
@@ -36,9 +36,68 @@ public sealed class ChannelViewModel(ChannelModel model) : ViewModelBase
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 Messages { get; } = new ObservableCollection(
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()
{
Model.Messages.Clear();
diff --git a/src/Decho/ViewModels/MainWindowViewModel.cs b/src/Decho/ViewModels/MainWindowViewModel.cs
index 118fb0c..1ab63d2 100644
--- a/src/Decho/ViewModels/MainWindowViewModel.cs
+++ b/src/Decho/ViewModels/MainWindowViewModel.cs
@@ -6,6 +6,7 @@ using Decho.Views;
using EchoHub.Client.Commands;
using EchoHub.Client.Config;
+using EchoHub.Client.Services;
using EchoHub.Core.Constants;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
@@ -22,6 +23,7 @@ namespace Decho.ViewModels;
public sealed class MainWindowViewModel : ViewModelBase
{
private readonly CommandHandler _commandHandler;
+ private readonly NotificationSoundService _notificationService;
private Window? _mainWindow;
public string Title { get; } = "Decho";
@@ -44,6 +46,7 @@ public sealed class MainWindowViewModel : ViewModelBase
{
ConnectionService = new ConnectionService();
_commandHandler = new CommandHandler();
+ _notificationService = new NotificationSoundService(ConfigManager.Load().Notifications);
Sidebar = new SidebarViewModel();
Chat = new ChatViewModel();
@@ -360,7 +363,7 @@ public sealed class MainWindowViewModel : ViewModelBase
await ConnectionService.NukeChannelAsync(serverUrl, channel);
};
- _commandHandler.OnTestSound += () => Task.CompletedTask;
+ _commandHandler.OnTestSound += _notificationService.PlayTestAsync;
_commandHandler.OnQuit += () =>
{
@@ -554,11 +557,29 @@ public sealed class MainWindowViewModel : ViewModelBase
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(() =>
{
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) =>
@@ -646,6 +667,8 @@ public sealed class MainWindowViewModel : ViewModelBase
return;
}
+ channel.ClearUnread();
+
string serverUrl = FindServerUrlForChannel(channel);
bool isServerConnected = Sidebar.GetServer(serverUrl)?.IsConnected ?? false;
Chat.SetChannel(channel, serverUrl, isServerConnected);
diff --git a/src/Decho/ViewModels/MessageViewModel.cs b/src/Decho/ViewModels/MessageViewModel.cs
index 42fee20..2eeaf35 100644
--- a/src/Decho/ViewModels/MessageViewModel.cs
+++ b/src/Decho/ViewModels/MessageViewModel.cs
@@ -25,12 +25,12 @@ public sealed class MessageViewModel(MessageModel model) : ViewModelBase
return Brushes.White;
}
- try
- {
+ try
+ {
return new SolidColorBrush(Color.Parse(color));
}
catch
- {
+ {
return null;
}
}
diff --git a/src/Decho/Views/ChannelListView.axaml b/src/Decho/Views/ChannelListView.axaml
index 21b7d5b..c6f5490 100644
--- a/src/Decho/Views/ChannelListView.axaml
+++ b/src/Decho/Views/ChannelListView.axaml
@@ -12,12 +12,24 @@
SelectedItem="{Binding SelectedChannel, Mode=TwoWay}">
-
-
-
-
-
-
+
+
+
+
+
+
+