Stop repeated preview push notifications
This commit is contained in:
@@ -5,6 +5,7 @@ using QMax.Api.Data.Entities;
|
|||||||
using QMax.Api.Infrastructure.Hubs;
|
using QMax.Api.Infrastructure.Hubs;
|
||||||
using QMax.Api.Infrastructure.Max;
|
using QMax.Api.Infrastructure.Max;
|
||||||
using QMax.Api.Infrastructure.Storage;
|
using QMax.Api.Infrastructure.Storage;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace QMax.Api.Services;
|
namespace QMax.Api.Services;
|
||||||
@@ -15,6 +16,8 @@ public sealed class MaxBridgeSyncService(
|
|||||||
IHubContext<QMaxHub> hubContext,
|
IHubContext<QMaxHub> hubContext,
|
||||||
ILogger<MaxBridgeSyncService> logger)
|
ILogger<MaxBridgeSyncService> logger)
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, DateTimeOffset> PreviewNotifications = new();
|
||||||
|
|
||||||
public async Task<int> SyncOnceAsync(CancellationToken cancellationToken)
|
public async Task<int> SyncOnceAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -268,7 +271,7 @@ public sealed class MaxBridgeSyncService(
|
|||||||
|
|
||||||
var previousPreview = chat.LastMessagePreview;
|
var previousPreview = chat.LastMessagePreview;
|
||||||
var previousPreviewAt = chat.LastMessageAt;
|
var previousPreviewAt = chat.LastMessageAt;
|
||||||
var previewAt = ResolveListPreviewAt(update);
|
var previewAt = ResolveListPreviewAt(update, previousPreviewAt ?? chat.UpdatedAt);
|
||||||
var samePreview = string.Equals(previousPreview, preview, StringComparison.Ordinal);
|
var samePreview = string.Equals(previousPreview, preview, StringComparison.Ordinal);
|
||||||
var samePreviewAt = previousPreviewAt is not null &&
|
var samePreviewAt = previousPreviewAt is not null &&
|
||||||
Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1;
|
Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1;
|
||||||
@@ -281,11 +284,13 @@ public sealed class MaxBridgeSyncService(
|
|||||||
chat.LastMessageAt = previewAt;
|
chat.LastMessageAt = previewAt;
|
||||||
chat.UpdatedAt = DateTimeOffset.UtcNow;
|
chat.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
var shouldNotify = !chatWasCreated &&
|
var shouldNotifyCandidate = !chatWasCreated &&
|
||||||
!string.IsNullOrWhiteSpace(previousPreview) &&
|
!string.IsNullOrWhiteSpace(previousPreview) &&
|
||||||
update.LastMessageIsOutgoing != true &&
|
update.LastMessageIsOutgoing != true &&
|
||||||
update.Messages.Count == 0 &&
|
update.Messages.Count == 0 &&
|
||||||
IsFreshListPreview(update, previewAt);
|
IsFreshListPreview(update, previewAt);
|
||||||
|
var shouldNotify = shouldNotifyCandidate &&
|
||||||
|
RememberPreviewNotification(chat.Id, preview, previewAt);
|
||||||
|
|
||||||
if (incrementUnread && shouldNotify)
|
if (incrementUnread && shouldNotify)
|
||||||
{
|
{
|
||||||
@@ -313,16 +318,36 @@ public sealed class MaxBridgeSyncService(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DateTimeOffset ResolveListPreviewAt(MaxChatUpdate update)
|
private static bool RememberPreviewNotification(Guid chatId, string preview, DateTimeOffset previewAt)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
if (PreviewNotifications.Count > 4096)
|
||||||
|
{
|
||||||
|
var cutoff = now.AddHours(-2);
|
||||||
|
foreach (var item in PreviewNotifications)
|
||||||
|
{
|
||||||
|
if (item.Value < cutoff)
|
||||||
|
{
|
||||||
|
PreviewNotifications.TryRemove(item.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var previewMinute = previewAt.ToUniversalTime().Ticks / TimeSpan.TicksPerMinute;
|
||||||
|
var key = $"{chatId:N}|{previewMinute}|{preview.Trim().ToLowerInvariant()}";
|
||||||
|
return PreviewNotifications.TryAdd(key, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset ResolveListPreviewAt(MaxChatUpdate update, DateTimeOffset? fallback = null)
|
||||||
{
|
{
|
||||||
return TryParseMoscowListTime(update.LastMessageTimeText, out var parsed)
|
return TryParseMoscowListTime(update.LastMessageTimeText, out var parsed)
|
||||||
? parsed
|
? parsed
|
||||||
: update.LastMessageAt ?? update.UpdatedAt;
|
: fallback ?? (update.Messages.Count > 0 ? update.LastMessageAt : null) ?? update.UpdatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsFreshListPreview(MaxChatUpdate update, DateTimeOffset previewAt)
|
private static bool IsFreshListPreview(MaxChatUpdate update, DateTimeOffset previewAt)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(update.LastMessageTimeText))
|
if (!HasListClockTime(update.LastMessageTimeText))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -331,6 +356,11 @@ public sealed class MaxBridgeSyncService(
|
|||||||
return previewAt >= now.AddMinutes(-15) && previewAt <= now.AddMinutes(10);
|
return previewAt >= now.AddMinutes(-15) && previewAt <= now.AddMinutes(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasListClockTime(string? value)
|
||||||
|
{
|
||||||
|
return System.Text.RegularExpressions.Regex.IsMatch(value?.Trim() ?? "", @"\b\d{1,2}:\d{2}\b");
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryParseMoscowListTime(string? value, out DateTimeOffset parsed)
|
private static bool TryParseMoscowListTime(string? value, out DateTimeOffset parsed)
|
||||||
{
|
{
|
||||||
parsed = default;
|
parsed = default;
|
||||||
|
|||||||
Reference in New Issue
Block a user