diff --git a/src/ST.Client.Desktop.Avalonia/Application/UI/Views/MainView.axaml b/src/ST.Client.Desktop.Avalonia/Application/UI/Views/MainView.axaml index 818b22273..628e705d6 100644 --- a/src/ST.Client.Desktop.Avalonia/Application/UI/Views/MainView.axaml +++ b/src/ST.Client.Desktop.Avalonia/Application/UI/Views/MainView.axaml @@ -162,7 +162,8 @@ FallbackSource="avares://System.Application.SteamTools.Client.Avalonia/Application/UI/Assets/AppResources/avatar.jpg"> - diff --git a/src/ST.Client.Desktop.Avalonia/Application/UI/Views/Pages/UserCenter/UserItem.axaml b/src/ST.Client.Desktop.Avalonia/Application/UI/Views/Pages/UserCenter/UserItem.axaml index 14ff26b7a..dbb514ab8 100644 --- a/src/ST.Client.Desktop.Avalonia/Application/UI/Views/Pages/UserCenter/UserItem.axaml +++ b/src/ST.Client.Desktop.Avalonia/Application/UI/Views/Pages/UserCenter/UserItem.axaml @@ -126,7 +126,9 @@ CommandParameter="Notice"> - - --> - @@ -94,20 +95,34 @@ Margin="10" IsVisible="{Binding Path=!IsEmpty,Mode=OneWay}"> - + - + + + + + @@ -140,7 +155,7 @@ DecodeWidth="{ReflectionBinding $parent.Width}" Margin="10 0" Source="{Binding NoticeItem.PictureStream^}" - IsVisible="{Binding NoticeItem.PictureStream^,Converter={StaticResource IsNullConverter},ConverterParameter=invert}}" + IsVisible="{Binding NoticeItem.PictureStream,Converter={StaticResource IsNullConverter},ConverterParameter=invert}}" Stretch="Uniform" /> ("TitleBarHost") is IControl t) { @@ -109,8 +109,7 @@ namespace Avalonia.Controls //ExtendClientAreaToDecorationsHint = true; //ExtendClientAreaTitleBarHeightHint = -1; - ExtendClientAreaChromeHints = - ExtendClientAreaChromeHints.Default; + ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; } else { @@ -131,8 +130,7 @@ namespace Avalonia.Controls } else { - ExtendClientAreaChromeHints = - ExtendClientAreaChromeHints.SystemChrome; + ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.Default; } #pragma warning restore CA1416 // 验证平台兼容性 diff --git a/src/ST.Client/Entities/Notification.cs b/src/ST.Client/Entities/Notification.cs new file mode 100644 index 000000000..dc369493e --- /dev/null +++ b/src/ST.Client/Entities/Notification.cs @@ -0,0 +1,30 @@ +using SQLite; +using System.Application.Columns; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using NotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; +using SQLiteNotNull = SQLite.NotNullAttribute; +using SQLiteTable = SQLite.TableAttribute; + +namespace System.Application.Entities +{ + /// + /// 通知记录 + /// + [SQLiteTable("A765AD32")] + [DebuggerDisplay("{DebuggerDisplay(),nq}")] + public class Notification : IEntity + { + string DebuggerDisplay => Id.ToString(); + + [Column("2ABD43F2")] + [PrimaryKey] + public Guid Id { get; set; } + + [Column("4557AC21")] + public DateTimeOffset ExpirationTime { get; set; } = DateTimeOffset.Now; + + [Column("8E7393C5")] + public bool HasRead { get; set; } + } +} diff --git a/src/ST.Client/KeyConstants.cs b/src/ST.Client/KeyConstants.cs index 886a2326f..61451d7e0 100644 --- a/src/ST.Client/KeyConstants.cs +++ b/src/ST.Client/KeyConstants.cs @@ -6,10 +6,5 @@ namespace System.Application /// 当前登录用户 /// public const string KEY_CURRENT_LOGIN_USER = "KEY_CURRENT_LOGIN_USER"; - - /// - /// 上次启动时收到的通知纪录 ID - /// - public const string ON_LAST_STARTUP_NOTIFICATION_RECORD_ID = "ON_LAST_STARTUP_NOTIFICATION_RECORD_ID"; } } \ No newline at end of file diff --git a/src/ST.Client/Repositories/INotificationRepository.cs b/src/ST.Client/Repositories/INotificationRepository.cs new file mode 100644 index 000000000..51171ca06 --- /dev/null +++ b/src/ST.Client/Repositories/INotificationRepository.cs @@ -0,0 +1,15 @@ +using System.Application.Entities; +using System.Application.Models; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace System.Application.Repositories +{ + public interface INotificationRepository : IRepository + { + + Task> GetAllAsync(Expression>? expression = null); + } +} diff --git a/src/ST.Client/Repositories/Implementation/NotificationRepository.cs b/src/ST.Client/Repositories/Implementation/NotificationRepository.cs new file mode 100644 index 000000000..c87095e17 --- /dev/null +++ b/src/ST.Client/Repositories/Implementation/NotificationRepository.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using System.Application.Entities; +using System.Application.Models; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace System.Application.Repositories.Implementation +{ + internal sealed class NotificationRepository : Repository, INotificationRepository + { + public async Task> GetAllAsync(Expression>? expression = null) + { + var dbConnection = await GetDbConnection().ConfigureAwait(false); + return await AttemptAndRetry(async () => + { + var r = dbConnection.Table(); + if (expression != null) + r = r.Where(expression); + return await r.ToArrayAsync(); + }).ConfigureAwait(false); + } + } +} diff --git a/src/ST.Client/ServiceCollectionExtensions.cs b/src/ST.Client/ServiceCollectionExtensions.cs index 30a152b03..0a8a317f2 100644 --- a/src/ST.Client/ServiceCollectionExtensions.cs +++ b/src/ST.Client/ServiceCollectionExtensions.cs @@ -20,6 +20,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/ST.Client/Services/INotificationService.cs b/src/ST.Client/Services/INotificationService.cs index 28bec654c..9d88e709f 100644 --- a/src/ST.Client/Services/INotificationService.cs +++ b/src/ST.Client/Services/INotificationService.cs @@ -101,14 +101,6 @@ namespace System.Application.Services } - /// - /// 获取最后一次收到的通知启用时间 只提示大于此时间的通知 - /// - /// - static Task GetLastNotificationTime() => ISecureStorage.Instance.GetAsync(ON_LAST_STARTUP_NOTIFICATION_RECORD_ID); - - static Task SetLastNotificationTime(DateTimeOffset time) => ISecureStorage.Instance.SetAsync(ON_LAST_STARTUP_NOTIFICATION_RECORD_ID, time); - /// /// NotifyIcon / TrayIcon 右下角托盘菜单助手类 /// diff --git a/src/ST.Client/Services/Mvvm/NotificationService.cs b/src/ST.Client/Services/Mvvm/NotificationService.cs index 97cf850ba..56ebb7a2a 100644 --- a/src/ST.Client/Services/Mvvm/NotificationService.cs +++ b/src/ST.Client/Services/Mvvm/NotificationService.cs @@ -4,6 +4,7 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using System.Application.Models; using System.Application.Properties; +using System.Application.Repositories; using System.Application.Settings; using System.Application.UI.Resx; using System.Collections.Generic; @@ -30,6 +31,7 @@ namespace System.Application.Services public static NotificationService Current => mCurrent ?? new(); readonly IHttpService httpService = DI.Get(); + readonly INotificationRepository notificationRepository = DI.Get(); public SourceCache NoticesSource { get; } @@ -39,6 +41,8 @@ namespace System.Application.Services [Reactive] public ObservableCollection NoticeTypes { get; set; } + public NoticeTypeDTO DefaultType { get; } = new NoticeTypeDTO() { Name = AppResources.AllTyoe }; + public NotificationService() { mCurrent = this; @@ -48,38 +52,39 @@ namespace System.Application.Services public async Task GetNews(int trycount = 0) { - var lastTime = await INotificationService.GetLastNotificationTime(); + var data = await notificationRepository.GetAllAsync(w => w.ExpirationTime > DateTimeOffset.Now); + UnreadNotificationsCount = data.Count(x => !x.HasRead); var client = ICloudServiceClient.Instance.Notice; var result = await client.NewMsg(null, null); if (result.IsSuccess && result.Content != null) { - var noticeList = result.Content.Where(x => lastTime.HasValue ? x.EnableTime > lastTime : true); - if (noticeList.Count() > 1) + var noticeList = result.Content.Where(x => !data.Any(d => d.Id == x.Id)); + if (noticeList.Any_Nullable()) { - await INotificationService.SetLastNotificationTime(noticeList.Max(x => x.EnableTime)); - INotificationService.Instance.Notify(new NotificationBuilder() + if (noticeList.Count() > 1) { - Title = AppResources.NotificationChannelType_Description_Announcement, - Content = AppResources.Notice_Tray.Format(result.Content.Length), - AutoCancel = NotificationBuilder.DefaultAutoCancel, - Type = NotificationType.Announcement, - Click = new NotificationBuilder.ClickAction(() => + INotificationService.Instance.Notify(new NotificationBuilder() { - IWindowManager.Instance.Show(CustomWindow.Notice); - }) - }); - } - else - { - var notice = noticeList.FirstOrDefault(); - if (notice != null && notice.EnableTime > lastTime) + Title = AppResources.Notice_Tray_Title, + Content = AppResources.Notice_Tray_Content.Format(result.Content.Length), + AutoCancel = NotificationBuilder.DefaultAutoCancel, + Type = NotificationType.Announcement, + Click = new NotificationBuilder.ClickAction(() => + { + IWindowManager.Instance.Show(CustomWindow.Notice); + }) + }); + } + else { - await INotificationService.SetLastNotificationTime(notice.EnableTime); + var notice = noticeList.First(); INotificationService.Instance.Notify(notice); } + await InsertNotificationRecord(noticeList); } + DeleteExpiredRecordAsync(); } } @@ -90,6 +95,7 @@ namespace System.Application.Services if (result.IsSuccess) { NoticeTypes.Clear(); + NoticeTypes.Add(DefaultType); NoticeTypes.AddRange(result.Content!.OrderBy(x => x.Order)); } } @@ -97,7 +103,9 @@ namespace System.Application.Services public async Task LoadNotification(NoticeTypeDTO? selectGroup) { var client = ICloudServiceClient.Instance.Notice; - var result = await client.Table(selectGroup?.Id, 1, 30); + var typeid = selectGroup == DefaultType ? null : selectGroup?.Id; + var isAll = typeid is null; + var result = await client.Table(typeid, 1, 30); if (result.IsSuccess && result.Content != null) { //foreach (var item in result.Content!.DataSource) @@ -107,7 +115,59 @@ namespace System.Application.Services //} NoticesSource.Clear(); NoticesSource.AddOrUpdate(result.Content.DataSource); + if (isAll) + { + await LoadHasReadRecord(); + } } } + + public async Task LoadHasReadRecord() + { + var data = await notificationRepository.GetAllAsync(w => w.ExpirationTime > DateTimeOffset.Now); + if (data.Any_Nullable()) + { + //var oldData = data.Where(w => w.ExpirationTime > DateTimeOffset.Now); + //if (oldData.Any()) + // await notificationRepository.DeleteRangeAsync(oldData); + UnreadNotificationsCount = data.Count(x => !x.HasRead); + + foreach (var d in data) + { + var notice = NoticesSource.Lookup(d.Id); + if (notice.HasValue) + notice.Value.HasRead = d.HasRead; + } + } + } + + public async Task InsertNotificationRecord(IEnumerable notices) + { + var data = await notificationRepository.GetAllAsync(w => w.ExpirationTime > DateTimeOffset.Now); + var newData = notices + .Where(w => !data.Any(d => d.Id == w.Id)) + .Select(s => new Entities.Notification() + { + Id = s.Id, + HasRead = false, + ExpirationTime = s.OverdueTime, + }); + var num = await notificationRepository.InsertRangeAsync(newData); + if (num > 0) + { + UnreadNotificationsCount += num; + } + } + + public async Task MarkNotificationHasRead(params Entities.Notification[] notice) + { + await notificationRepository.UpdateRangeAsync(notice); + } + + public async void DeleteExpiredRecordAsync() + { + var data = await notificationRepository.GetAllAsync(w => w.ExpirationTime < DateTimeOffset.Now); + await notificationRepository.DeleteRangeAsync(data); + } } } diff --git a/src/ST.Client/UI/Resx/AppResources.Designer.cs b/src/ST.Client/UI/Resx/AppResources.Designer.cs index 8772086bf..b582aa9af 100644 --- a/src/ST.Client/UI/Resx/AppResources.Designer.cs +++ b/src/ST.Client/UI/Resx/AppResources.Designer.cs @@ -515,6 +515,15 @@ namespace System.Application.UI.Resx { } } + /// + /// 查找类似 全部类型 的本地化字符串。 + /// + public static string AllTyoe { + get { + return ResourceManager.GetString("AllTyoe", resourceCulture); + } + } + /// /// 查找类似 和 的本地化字符串。 /// @@ -4069,11 +4078,38 @@ namespace System.Application.UI.Resx { } /// - /// 查找类似 当前有 {0} 条{1}通知,详情请在程序内通知公告处查看。 的本地化字符串。 + /// 查找类似 全部标记为已读 的本地化字符串。 /// - public static string Notice_Tray { + public static string Notice_AllMarkHasRead { get { - return ResourceManager.GetString("Notice_Tray", resourceCulture); + return ResourceManager.GetString("Notice_AllMarkHasRead", resourceCulture); + } + } + + /// + /// 查找类似 收到{0}条新消息,详情请在程序内我的消息处查看。 的本地化字符串。 + /// + public static string Notice_Tray_Content { + get { + return ResourceManager.GetString("Notice_Tray_Content", resourceCulture); + } + } + + /// + /// 查找类似 消息提示 的本地化字符串。 + /// + public static string Notice_Tray_Title { + get { + return ResourceManager.GetString("Notice_Tray_Title", resourceCulture); + } + } + + /// + /// 查找类似 消息类型 的本地化字符串。 + /// + public static string Notice_Type { + get { + return ResourceManager.GetString("Notice_Type", resourceCulture); } } diff --git a/src/ST.Client/UI/Resx/AppResources.resx b/src/ST.Client/UI/Resx/AppResources.resx index 1b3b1239e..53d1d7179 100644 --- a/src/ST.Client/UI/Resx/AppResources.resx +++ b/src/ST.Client/UI/Resx/AppResources.resx @@ -2272,8 +2272,11 @@ AAAAA-BBBBB-CCCC3 请正确选择一张要应用的图片 - - 当前有 {0} 条{1}通知,详情请在程序内通知公告处查看。 + + 消息提示 + + + 收到{0}条新消息,详情请在程序内我的消息处查看。 证书状态 @@ -2365,4 +2368,13 @@ AAAAA-BBBBB-CCCC3 查看详情 + + 全部类型 + + + 全部标记为已读 + + + 消息类型 + \ No newline at end of file diff --git a/src/ST.Client/UI/ViewModels/Windows/UserCenter/NoticeWindowViewModel.cs b/src/ST.Client/UI/ViewModels/Windows/UserCenter/NoticeWindowViewModel.cs index f4058dfb3..c72b476d9 100644 --- a/src/ST.Client/UI/ViewModels/Windows/UserCenter/NoticeWindowViewModel.cs +++ b/src/ST.Client/UI/ViewModels/Windows/UserCenter/NoticeWindowViewModel.cs @@ -39,9 +39,20 @@ namespace System.Application.UI.ViewModels { Title = GetTitleByDisplayName(DisplayName); //OpenNotice = ReactiveCommand.CreateFromTask(x => Browser2.OpenAsync(x)); - MarkAllRead = ReactiveCommand.Create(() => + MarkAllRead = ReactiveCommand.CreateFromTask(async () => { + var data = NotificationService.Current.NoticesSource.Items.Select(s => new Entities.Notification + { + Id = s.Id, + ExpirationTime = s.OverdueTime, + HasRead = true + }).ToArray(); + await NotificationService.Current.MarkNotificationHasRead(data); + foreach (var item in NotificationService.Current.NoticesSource.Items) + { + item.HasRead = true; + } }); NotificationService.Current.NoticesSource @@ -62,6 +73,21 @@ namespace System.Application.UI.ViewModels await NotificationService.Current.LoadNotification(x); IsLoading = false; }); + + this.WhenValueChanged(x => x.NoticeItem) + .Subscribe(x => + { + if (x != null && !x.HasRead) + { + _ = NotificationService.Current.MarkNotificationHasRead(new Entities.Notification + { + Id = x.Id, + ExpirationTime = x.OverdueTime, + HasRead = true + }); + x.HasRead = true; + } + }); } public override async void Activation() @@ -69,6 +95,7 @@ namespace System.Application.UI.ViewModels //if (IsFirstActivation) IsLoading = true; await NotificationService.Current.LoadNoticeTypes(); + SelectType = NotificationService.Current.NoticeTypes.FirstOrDefault(); await NotificationService.Current.LoadNotification(SelectType); IsLoading = false; base.Activation(); diff --git a/src/ST.Services.CloudService.Models/Models/Notice/NoticeDTO.cs b/src/ST.Services.CloudService.Models/Models/Notice/NoticeDTO.cs index c630dc5ff..82ac2a780 100644 --- a/src/ST.Services.CloudService.Models/Models/Notice/NoticeDTO.cs +++ b/src/ST.Services.CloudService.Models/Models/Notice/NoticeDTO.cs @@ -102,16 +102,6 @@ namespace System.Application.Models [S_JsonProperty("9")] public DateTimeOffset OverdueTime { get; set; } -#if MVVM_VM - /// - /// 是否过期 - /// - [MPIgnore] - [N_JsonIgnore] - [S_JsonIgnore] - public bool Overdue => DateTimeOffset.Now > OverdueTime; -#endif - /// /// 通知渠道 /// @@ -129,5 +119,24 @@ namespace System.Application.Models [N_JsonProperty("12")] [S_JsonProperty("12")] public string? Context { get; set; } = string.Empty; + +#if MVVM_VM + /// + /// 是否过期 + /// + [MPIgnore] + [N_JsonIgnore] + [S_JsonIgnore] + public bool Overdue => DateTimeOffset.Now > OverdueTime; + + /// + /// 是否已读 + /// + [MPIgnore] + [N_JsonIgnore] + [S_JsonIgnore] + [Reactive] + public bool HasRead { get; set; } +#endif } }