通知功能完善

This commit is contained in:
RMBGAME
2022-07-20 15:44:32 +08:00
parent 29e1e79816
commit bf4df45ba0
15 changed files with 281 additions and 63 deletions
@@ -162,7 +162,8 @@
FallbackSource="avares://System.Application.SteamTools.Client.Avalonia/Application/UI/Assets/AppResources/avatar.jpg">
</gif:Image2>
</Border>
<ui:InfoBadge Classes="message"
<ui:InfoBadge Classes="Critical message"
IsVisible="{ReflectionBinding Source={x:Static service:NotificationService.Current},Path=UnreadNotificationsCount,Converter={StaticResource IsNullConverter},ConverterParameter=invert,Mode=OneWay}"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"/>
</Panel>
@@ -126,7 +126,9 @@
CommandParameter="Notice">
<ui:Button.Content>
<DockPanel>
<ui:InfoBadge Value="3"
<ui:InfoBadge Value="{ReflectionBinding Source={x:Static service:NotificationService.Current},Path=UnreadNotificationsCount,Mode=OneWay}"
IsVisible="{ReflectionBinding Source={x:Static service:NotificationService.Current},Path=UnreadNotificationsCount,Converter={StaticResource IsNullConverter},ConverterParameter=invert,Mode=OneWay}"
Classes="Critical"
DockPanel.Dock="Right"
HorizontalAlignment="Right" />
<TextBlock Text="{ReflectionBinding Path=Res.NotificationChannelType_Name_Announcement,Mode=OneWay,Source={x:Static resx:R.Current}}"
@@ -31,7 +31,7 @@
<ui:CommandBarElementContainer Margin="5 0">
<WrapPanel VerticalAlignment="Center">
<TextBlock Text="消息类型:"
<TextBlock Text="{ReflectionBinding Path=Res.Notice_Type,StringFormat={}{0}:,Mode=OneWay,Source={x:Static resx:R.Current}}"
VerticalAlignment="Center"/>
<ComboBox VerticalAlignment="Center"
Items="{ReflectionBinding Source={x:Static service:NotificationService.Current},Path=NoticeTypes,Mode=OneWay}"
@@ -58,7 +58,8 @@
</ui:CommandBarButton.Icon>
</ui:CommandBarButton>-->
<ui:CommandBarButton Label="全部标记为已读"
<ui:CommandBarButton Label="{ReflectionBinding Path=Res.Notice_AllMarkHasRead,Mode=OneWay,Source={x:Static resx:R.Current}}"
IsEnabled="{ReflectionBinding Source={x:Static service:NotificationService.Current},Path=UnreadNotificationsCount,Converter={StaticResource IsNullConverter},ConverterParameter=invert,Mode=OneWay}"
Command="{Binding MarkAllRead,Mode=OneTime}">
<ui:CommandBarButton.Icon>
<ui:SymbolIcon Symbol="Checkmark"/>
@@ -94,20 +95,34 @@
Margin="10"
IsVisible="{Binding Path=!IsEmpty,Mode=OneWay}">
<Panel>
<Grid ColumnDefinitions="3*,Auto,7*">
<Grid ColumnDefinitions="4*,Auto,7*">
<Border>
<ListBox Items="{Binding Notices}"
SelectedItem="{Binding NoticeItem,Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:NoticeDTO">
<StackPanel Margin="10" Spacing="8">
<TextBlock Text="{Binding Title,Mode=OneWay}"
Classes="h6"
TextTrimming="CharacterEllipsis"/>
<DockPanel>
<ui:InfoBadge IsVisible="{Binding !HasRead}"
Classes="Critical"
Height="6"
Width="6"
DockPanel.Dock="Right"
HorizontalAlignment="Right" />
<TextBlock Text="{Binding Title,Mode=OneWay}"
Classes="h6"
TextTrimming="CharacterEllipsis"/>
</DockPanel>
<TextBlock
Text="{Binding Introduction}"
Foreground="{DynamicResource InactiveForegroundBrushKey}"
Classes="h8"
TextTrimming="CharacterEllipsis"/>
<TextBlock
Text="{Binding CreationTime,StringFormat={}{0:yyyy-MM-dd}}"
Foreground="{DynamicResource InactiveForegroundBrushKey}"
HorizontalAlignment="Right"
Classes="h8"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
@@ -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" />
<TextBlock TextWrapping="Wrap"
@@ -87,7 +87,7 @@ namespace Avalonia.Controls
{
TitleBar.ExtendViewIntoTitleBar = true;
//titleBar.LayoutMetricsChanged += OnApplicationTitleBarLayoutMetricsChanged;
//TitleBar.LayoutMetricsChanged += OnApplicationTitleBarLayoutMetricsChanged;
if (this.FindControl<Control>("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 // 验证平台兼容性
+30
View File
@@ -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
{
/// <summary>
/// 通知记录
/// </summary>
[SQLiteTable("A765AD32")]
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
public class Notification : IEntity<Guid>
{
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; }
}
}
-5
View File
@@ -6,10 +6,5 @@ namespace System.Application
/// 当前登录用户
/// </summary>
public const string KEY_CURRENT_LOGIN_USER = "KEY_CURRENT_LOGIN_USER";
/// <summary>
/// 上次启动时收到的通知纪录 ID
/// </summary>
public const string ON_LAST_STARTUP_NOTIFICATION_RECORD_ID = "ON_LAST_STARTUP_NOTIFICATION_RECORD_ID";
}
}
@@ -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<Notification, Guid>
{
Task<IList<Notification>> GetAllAsync(Expression<Func<Notification, bool>>? expression = null);
}
}
@@ -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<Notification, Guid>, INotificationRepository
{
public async Task<IList<Notification>> GetAllAsync(Expression<Func<Notification, bool>>? expression = null)
{
var dbConnection = await GetDbConnection().ConfigureAwait(false);
return await AttemptAndRetry(async () =>
{
var r = dbConnection.Table<Notification>();
if (expression != null)
r = r.Where(expression);
return await r.ToArrayAsync();
}).ConfigureAwait(false);
}
}
}
@@ -20,6 +20,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IGameAccountPlatformAuthenticatorRepository, GameAccountPlatformAuthenticatorRepository>();
services.AddSingleton<IScriptRepository, ScriptRepository>();
services.AddSingleton<INotificationRepository, NotificationRepository>();
return services;
}
@@ -101,14 +101,6 @@ namespace System.Application.Services
}
/// <summary>
/// 获取最后一次收到的通知启用时间 只提示大于此时间的通知
/// </summary>
/// <returns></returns>
static Task<DateTimeOffset?> GetLastNotificationTime() => ISecureStorage.Instance.GetAsync<DateTimeOffset?>(ON_LAST_STARTUP_NOTIFICATION_RECORD_ID);
static Task SetLastNotificationTime(DateTimeOffset time) => ISecureStorage.Instance.SetAsync<DateTimeOffset?>(ON_LAST_STARTUP_NOTIFICATION_RECORD_ID, time);
/// <summary>
/// NotifyIcon / TrayIcon 右下角托盘菜单助手类
/// </summary>
@@ -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<IHttpService>();
readonly INotificationRepository notificationRepository = DI.Get<INotificationRepository>();
public SourceCache<NoticeDTO, Guid> NoticesSource { get; }
@@ -39,6 +41,8 @@ namespace System.Application.Services
[Reactive]
public ObservableCollection<NoticeTypeDTO> 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<NoticeDTO> 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);
}
}
}
+39 -3
View File
@@ -515,6 +515,15 @@ namespace System.Application.UI.Resx {
}
}
/// <summary>
/// 查找类似 全部类型 的本地化字符串。
/// </summary>
public static string AllTyoe {
get {
return ResourceManager.GetString("AllTyoe", resourceCulture);
}
}
/// <summary>
/// 查找类似 和 的本地化字符串。
/// </summary>
@@ -4069,11 +4078,38 @@ namespace System.Application.UI.Resx {
}
/// <summary>
/// 查找类似 当前有 {0} 条{1}通知,详情请在程序内通知公告处查看。 的本地化字符串。
/// 查找类似 全部标记为已读 的本地化字符串。
/// </summary>
public static string Notice_Tray {
public static string Notice_AllMarkHasRead {
get {
return ResourceManager.GetString("Notice_Tray", resourceCulture);
return ResourceManager.GetString("Notice_AllMarkHasRead", resourceCulture);
}
}
/// <summary>
/// 查找类似 收到{0}条新消息,详情请在程序内我的消息处查看。 的本地化字符串。
/// </summary>
public static string Notice_Tray_Content {
get {
return ResourceManager.GetString("Notice_Tray_Content", resourceCulture);
}
}
/// <summary>
/// 查找类似 消息提示 的本地化字符串。
/// </summary>
public static string Notice_Tray_Title {
get {
return ResourceManager.GetString("Notice_Tray_Title", resourceCulture);
}
}
/// <summary>
/// 查找类似 消息类型 的本地化字符串。
/// </summary>
public static string Notice_Type {
get {
return ResourceManager.GetString("Notice_Type", resourceCulture);
}
}
+14 -2
View File
@@ -2272,8 +2272,11 @@ AAAAA-BBBBB-CCCC3</value>
<data name="SaveEditedAppInfo_SelectImageFailed" xml:space="preserve">
<value>请正确选择一张要应用的图片</value>
</data>
<data name="Notice_Tray" xml:space="preserve">
<value>当前有 {0} 条{1}通知,详情请在程序内通知公告处查看。</value>
<data name="Notice_Tray_Title" xml:space="preserve">
<value>消息提示</value>
</data>
<data name="Notice_Tray_Content" xml:space="preserve">
<value>收到{0}条新消息,详情请在程序内我的消息处查看。</value>
</data>
<data name="CommunityFix_CertificateStatus" xml:space="preserve">
<value>证书状态</value>
@@ -2365,4 +2368,13 @@ AAAAA-BBBBB-CCCC3</value>
<data name="SeeDetails" xml:space="preserve">
<value>查看详情</value>
</data>
<data name="AllTyoe" xml:space="preserve">
<value>全部类型</value>
</data>
<data name="Notice_AllMarkHasRead" xml:space="preserve">
<value>全部标记为已读</value>
</data>
<data name="Notice_Type" xml:space="preserve">
<value>消息类型</value>
</data>
</root>
@@ -39,9 +39,20 @@ namespace System.Application.UI.ViewModels
{
Title = GetTitleByDisplayName(DisplayName);
//OpenNotice = ReactiveCommand.CreateFromTask<string>(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();
@@ -102,16 +102,6 @@ namespace System.Application.Models
[S_JsonProperty("9")]
public DateTimeOffset OverdueTime { get; set; }
#if MVVM_VM
/// <summary>
/// 是否过期
/// </summary>
[MPIgnore]
[N_JsonIgnore]
[S_JsonIgnore]
public bool Overdue => DateTimeOffset.Now > OverdueTime;
#endif
/// <summary>
/// 通知渠道
/// </summary>
@@ -129,5 +119,24 @@ namespace System.Application.Models
[N_JsonProperty("12")]
[S_JsonProperty("12")]
public string? Context { get; set; } = string.Empty;
#if MVVM_VM
/// <summary>
/// 是否过期
/// </summary>
[MPIgnore]
[N_JsonIgnore]
[S_JsonIgnore]
public bool Overdue => DateTimeOffset.Now > OverdueTime;
/// <summary>
/// 是否已读
/// </summary>
[MPIgnore]
[N_JsonIgnore]
[S_JsonIgnore]
[Reactive]
public bool HasRead { get; set; }
#endif
}
}