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}"> - - - - - - + + + + + + +