From b7241dd0df70e8cd086fe206e2efda6ea2fcf2ec Mon Sep 17 00:00:00 2001 From: sevenhill Date: Sat, 4 Jul 2026 13:23:23 +0300 Subject: [PATCH] Stop repeated preview push notifications --- .../QMax.Api/Services/MaxBridgeSyncService.cs | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/server/QMax.Api/Services/MaxBridgeSyncService.cs b/server/QMax.Api/Services/MaxBridgeSyncService.cs index 94f9547..1048e35 100644 --- a/server/QMax.Api/Services/MaxBridgeSyncService.cs +++ b/server/QMax.Api/Services/MaxBridgeSyncService.cs @@ -5,6 +5,7 @@ using QMax.Api.Data.Entities; using QMax.Api.Infrastructure.Hubs; using QMax.Api.Infrastructure.Max; using QMax.Api.Infrastructure.Storage; +using System.Collections.Concurrent; using System.Text; namespace QMax.Api.Services; @@ -15,6 +16,8 @@ public sealed class MaxBridgeSyncService( IHubContext hubContext, ILogger logger) { + private static readonly ConcurrentDictionary PreviewNotifications = new(); + public async Task SyncOnceAsync(CancellationToken cancellationToken) { try @@ -268,7 +271,7 @@ public sealed class MaxBridgeSyncService( var previousPreview = chat.LastMessagePreview; var previousPreviewAt = chat.LastMessageAt; - var previewAt = ResolveListPreviewAt(update); + var previewAt = ResolveListPreviewAt(update, previousPreviewAt ?? chat.UpdatedAt); var samePreview = string.Equals(previousPreview, preview, StringComparison.Ordinal); var samePreviewAt = previousPreviewAt is not null && Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1; @@ -281,11 +284,13 @@ public sealed class MaxBridgeSyncService( chat.LastMessageAt = previewAt; chat.UpdatedAt = DateTimeOffset.UtcNow; - var shouldNotify = !chatWasCreated && + var shouldNotifyCandidate = !chatWasCreated && !string.IsNullOrWhiteSpace(previousPreview) && update.LastMessageIsOutgoing != true && update.Messages.Count == 0 && IsFreshListPreview(update, previewAt); + var shouldNotify = shouldNotifyCandidate && + RememberPreviewNotification(chat.Id, preview, previewAt); if (incrementUnread && shouldNotify) { @@ -313,16 +318,36 @@ public sealed class MaxBridgeSyncService( 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) ? parsed - : update.LastMessageAt ?? update.UpdatedAt; + : fallback ?? (update.Messages.Count > 0 ? update.LastMessageAt : null) ?? update.UpdatedAt; } private static bool IsFreshListPreview(MaxChatUpdate update, DateTimeOffset previewAt) { - if (string.IsNullOrWhiteSpace(update.LastMessageTimeText)) + if (!HasListClockTime(update.LastMessageTimeText)) { return false; } @@ -331,6 +356,11 @@ public sealed class MaxBridgeSyncService( 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) { parsed = default;