Stop repeated preview push notifications

This commit is contained in:
sevenhill
2026-07-04 13:23:23 +03:00
parent 459ac2bb82
commit b7241dd0df
@@ -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;